How does std::get work?

Me myself and I picture Me myself and I · Jun 18, 2013 · Viewed 10.3k times · Source

After trying to make a std::get<N>(std::tuple) method myself, I'm not so sure how it's implemented by compilers. I know std::tuple has a constructor like this,

tuple(Args&&... args);

But what exactly is args... assigned to? I think this is useful in order to know how std::get works because the arguments need to be placed somewhere in order to access them...

Answer

Yakk - Adam Nevraumont picture Yakk - Adam Nevraumont · Jun 18, 2013

Here is a crude toy implementation of a tuple-like class.

First, some metaprogramming boilerplate, to represent a sequence of integers:

template<int...> struct seq {};
template<int max, int... s> struct make_seq:make_seq< max-1, max-1, s... > {};
template<int... s> struct make_seq<0, s...> {
  typedef seq<s...> type;
};
template<int max> using MakeSeq = typename make_seq<max>::type;

Next, the tagged class that actually stores the data:

template<int x, typename Arg>
struct foo_storage {
  Arg data;
};

This tagging technique is a common pattern whenever we want to associate data with some tag at compile time (in this case, an integer). The tag (an int here) isn't used anywhere in the storage usually, it is just used to tag the storage.

foo_helper unpacks a sequence and a set of arguments into a bunch of foo_storage, and inherits from them in a linear fashion. This is a pretty common pattern -- if you are doing this a lot, you end up creating metaprogramming tools that do this for you:

template<typename Seq, typename... Args>
struct foo_helper {};
template<int s0, int... s, typename A0, typename... Args>
struct foo_helper<seq<s0, s...>, A0, Args...>:
  foo_storage<s0, A0>,
  foo_helper<seq<s...>, Args...>
{};

My crude tuple type, foo, creates a package of a sequence of indexes and the args, and passes it to the helper above. The helper then creates a bunch of tagged data holding parent classes:

template<typename... Args>
struct foo: foo_helper< MakeSeq<sizeof...(Args)>, Args... > {};

I removed everything from the body of foo, because it isn't needed to implement get.

get is pretty simple: we take the storage type (not the tuple type), and the explicit template argument N disambiguates which of the foo_storage<n, T> we are going to access. Now that we have the storage type, we simply return the data field:

template<int N, typename T>
T& get( foo_storage<N, T>& f )
 { return f.data; }
template<int N, typename T>
T const& get( foo_storage<N, T> const& f )
 { return f.data; }

We are using the overloading mechanisms of the C++ langauge to do the heavy lifting. When you call a function with a class instance, that instance as each of the parent classes are gone over to see if any of them can be made to match. With the N fixed, there is only one parent class that is a valid argument, so parent class (and hence T) is deduced automatically.

And finally, some basic test code:

#include <iostream>

int main() {
  foo<int, double> f;
  get<0>( f ) = 7;
  get<1>( f ) = 3.14;
  std::cout << get<0>(f) << "," << get<1>(f) << "\n";
}