Recently, I stumbled upon a C++14 feature which I hadn’t seen before. Baffled at
first, I needed to run through a couple of examples before I fully understood
what was going and why this new feature was so useful. As the title suggest, I’m
std::index_sequence and related objects. (Note: I ran across
this while perusing rpclib source code, specifically, the file
Here’s the sequence of examples that got me there. First, imagine you wanted a function that takes a tuple and returns a subset of said tuple, where the indexes of the elements you want to have in the subset are known at compile time. You would, of course, prefer to have these indexes passed as template arguments rather than as function arguments. Here’s a stab at how such a function could look like:
Looks simple, but it took me more than one go to get right. First, note the
decltype(auto) in the function signature. Spelling out the return
type would have been annoying otherwise (and I’m not even sure how to do that
were I restricted to C++03 features). Where I stumbled was the order of the
template arguments: At first, I had placed
indexes, which of
course breaks type deduction. This highlights the first problem: What if I had
more template arguments, maybe more variadic ones? Could I run into deduction
issues? Since I’m passing in a
std::tuple, template deduction gets a huge
hint: Whatever’s in the tuple defines
Args. But if there’s no tuple, maybe I’d
run into the case where automatic type deduction would conflate my
Is there a way I can properly demarcate the
indexes from the other
Well, yeah, that’s what this article is about, so how about we use
What’s occurring here? Instead of passing the template arguments to
tuple_subset directly, we instead pass an object of type
as a function argument. Now, we can fully rely on automatic type deduction, and
can reorder the template argument order as we like.
If you’re like me and learned C++ in the 90s, you might think: Wait, aren’t we
now creating a new object and pushing it onto the stack just to avoid some
template ambiguity? Well, possibly – but note that
tuple_subset() in this
example doesn’t actually define two arguments, it only lists two types! A smart
compiler can (and should) thus make the choice to not actually create an object
here, but optimize it out. The utility of using the (fictive) object to help
with type deduction is still there!
This is a good time to take a look at the C++ standard to see what we actually
have here. C++14 defines
std::integer_sequence as a compile-time
sequence of integers of type T (see
std::index_sequence is a specialization, where type T equals
it’s a sequence of
size_t values, which are typically used as indexes in C++.
Let’s ignore the other integer types for now.
<utility> defines some helper factories to create index
std::make_index_sequence<N>() creates a sequence 0, 1, …, N-1.
If we wanted to create a function that doesn’t return any subset, but truncates
tuples starting at the beginning, this would work:
OK, that was boring. More interesting is
std::index_sequence_for. Given a
variable-length type T, it creates an index sequence ‘0, 1, …, N-1’, where
N is the
sizeof()... value. My example of extracting subsets of a tuple is
now slowly losing its educational value, but for the sake of completing this
train of thought, here’s an unnecessarily complicated way of copying a tuple:
We return a subset of the original tuple, that’s actually the full set:
What this does is:
tuple_rube_goldberg_copy()with the original tuple
- Inside that function, automatically generate a list of sequences (at compile-time!) for the entire length of the tuple.
tuple_subset()not only with the original tuple, but also with a full list of indexes of all the tuple elements.
OK, nice. We can do fancy stuff with tuples that would have been painful with older C++ versions. But the last example in particular is also not very useful, so, no big loss there. Which takes me to the code I was originally reviewing:
We pass a functor and a tuple that contains arguments to a function, and say “please call the functions with the arguments that are in the tuple”. And we do that with two functions, each one-liners. This is elegant and efficient – and given move semantics and compile-time expansions, this is probably not all that much more inefficient than calling the functor directly. Nice!
What did I learn:
- Helping the compiler when it’s faced with multiple variadic template arguments is generally a good idea.
- Template- and compiler-magic doesn’t only happen when you see
<>. Function arguments can also be privy to compile-time optimization.
- We can unpack tuples or arbitrary length as arguments to functors at compile-time.