Implementing std::tuple from the Ground Up – Part 4: Getting Tuple Elements

January 28, 2015

tags: ,
one comment

We have a constructible tuple at this point, but we don’t have a way of getting tuple elements by index or in any other way. It’s time to implement the main accessor — get<> — which we use to read and write the tuple’s elements. There are two flavors of the get<> template: get by index and get by type, the latter being part of tuple‘s interface since C++ 14. We are going to implement the latter in terms of the former.

Here are the overloads we need for get<>:

template <size_t I, typename... Types>
??? get(tuple<Types...> const& tup);

template <size_t I, typename... Types>
??? get(tuple<Types...>& tup);

template <size_t I, typename... Types>
??? get(tuple<Types...>&& tup);

It’s pretty clear that the return types and contents of all three functions will be very similar. Let’s start with the return type — what should it be?

Exercise 9: Implement a metafunction type_at_index<I, Types…> which returns the type of the I‘th element in the type parameter pack …Types.

This is yet another boring variadic template recursion:

template <size_t I, typename Head, typename... Tail>
struct type_at_index
{
  using type = typename type_at_index<I-1, Tail...>::type;
};

template <typename Head, typename... Tail>
struct type_at_index<0, Head, Tail...>
{
  using type = Head;
};

template <size_t I, typename... Types>
using type_at_index_t = typename type_at_index<I, Types...>::type;

Great. We can now fill in the return types:

template <size_t I, typename... Types>
type_at_index_t<I, Types...> const& get(tuple<Types...> const& tup);

template <size_t I, typename... Types>
type_at_index_t<I, Types...>& get(tuple<Types...>& tup);

template <size_t I, typename... Types>
type_at_index_t<I, Types...>&& get(tuple<Types...>&& tup);

Exercise 10: The third declaration is wrong. What’s wrong with it?

Well, if type_at_index returns an lvalue reference type, then the return type is going to be an lvalue reference as well because of the reference collapsing rules (& && becomes &)! For example:

int x = 42;
tuple<int&> t(x);
get<0>(std::move(t)); // returns int&!

This is easily fixable — we just need to remove the reference qualifier:

template <size_t I, typename... Types>
std::remove_reference_t<type_at_index_t<I, Types...>>&& get(tuple<Types...>&& tup);

OK, now how about the bodies of these methods? We designed tuple from the outset to make the implementation easy. Our tuple_elements are already indexed, so we just need to cast the tuple instance to the appropriate base class, and extract the value_ field. For example:

template <size_t I, typename... Types>
type_at_index<I, Types...>& get(tuple<Types...>& tup)
{
  tuple_element<I, type_at_index_t<I, Types...>>& base = tup;
  return base.value_;
}

Awesome. The other two methods are obviously similar. Now let’s take a look at the other kind of get — get<T>. It needs to verify that the type exists exactly once in the tuple, and return the element of that type.

tuple<int, string> t1;
get<int>(t1) = 42;
get<float>(t1) = 3.14f; // compilation error

tuple<int, int> t2;
get<int>(t2) = 42; // compilation error

Our basic approach will be as follows: count the number of times T appears in the tuple. If it is not exactly 1, bail with a compilation error. Otherwise, find the index I at which T appears and call get<I>. And again we’re going to use constexpr functions for most of the work — it’s so much easier and more elegant than using structs:

template <typename>
constexpr int count()
{
  return 0;
}
template <typename T, typename Head, typename... Tail>
constexpr int count()
{
  return (std::is_same<T, Head>::value ? 1 : 0) + count<T, Tail...>();
}

template <typename>
constexpr int find(int)
{
  return -1;
}
template <typename T, typename Head, typename... Tail>
constexpr int find(int current_index = 0)
{
  return std::is_same<T, Head>::value
    ? current_index
    : find<T, Tail...>(current_index + 1);
}

template <typename T, typename... Types>
T& get(tuple<Types...>& tup)
{
  static_assert(count<T, Types...>() == 1, "T must appear exactly once in ...Types");
  get<find<T, Types...>()>(tup);
}

Hopefully by now you’re starting to appreciate how beautiful, elegant, and concise tuples can be. They really bring the best out of metaprogramming and give us the opportunity to experiment with a bunch of various techniques.

As homework, you can implement some additional functionality on our tuple class, which I am too lazy to do here. For example:

* Copy and move assignment operators from tuple
* Comparison operators: ==, !=, <, <=, >, >=
* Tuple construction from std::pair and std::array

In our next installment we’re going to deal with a couple of simple non-member functions before moving on to tuple_cat, which will be the crown jewel of this series.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

one comment

  1. Daniel ParkerApril 26, 2017 ב 1:05 AM

    Nice article, thanks. But I think

    template
    type_at_index& get(tuple& tup)
    {
    ...
    }

    should be

    template
    typename type_at_index::type& get(tuple& tup)
    {
    ...

    Reply