The differences between tie, make_tuple, forward_as_tuple: How to Build a Tuple in C++?
Tuples are handy C++ components that appeared in C++11, and are a very useful help when programming with variadic templates.
To make things even simpler, C++ offers not one but three helpers to build tuples and make our variadic template code more expressive: std::make_tuple
, std::tie
and std::forward_as_tuple
. All three reflect in their name the fact that they put values together to build a tuple.
But why are there three of them? It can’t be so complicated to build a tuple, right?
It turns out that those three functions help craft different sorts of tuples, and perhaps even more importantly, if in a given situation you don’t use the right one, then you may be good for undefined behaviour.
What, Undefined Behaviour, just for assembling a handful of values into a tuple?
Yes. Let’s see what this is all about.
Undefined behaviour when building a tuple the wrong way
Consider the following example of a class X
that contains a tuple:
template<typename... Ts> class X { public: explicit X(Ts const&... values); std::tuple<Ts const&...> values_; };
values_
is a tuple of references (which is a legal thing, and can be useful–they came in handy in the smart output iterators library for example). This class holds references to the objects that are passed to its constructor.
Let’s try to implement the constructor.
The constructor of X
receives a variadic pack of values, and has to create a std::tuple
out of them. So let’s use… std::make_tuple
then! This sounds like it could make a tuple for us, doesn’t it?
template<typename... Ts> class X { public: explicit X(Ts const&... values) : values_(std::make_tuple(values...)) {} std::tuple<Ts const&...> values_; };
Okay. Let’s now try to use our class, with an int
and a std::string
for example:
int main() { int i = 42; auto s = std::string("universe"); auto x = X<int, std::string>(i, s); std::cout << "i = " << std::get<0>(x.values_) << '\n'; std::cout << "s = " << std::get<1>(x.values_) << '\n'; }
If all goes well, this program should output 42
and universe
, because those are the contents of the tuple, right?
Here is what this program outputs:
i = -1690189040 s =
Not quite what we wanted. This is undefined behaviour. Here is the whole snippet if you’d like to play around with it.
To understand what is going on, we need to understand what std::make_tuple
does, and what we should have used instead to make this code behave like we would have expected it (hint: we should have used std::tie
).
std::make_tuple
As it appears in the previous example, std::make_tuple
doesn’t just make a tuple. It contains some logic to determine the types of the values inside of the tuple it makes.
More specifically, std::make_tuple
applies std::decay
on each of the types it receives, in order to determine the corresponding type to store in the tuple. And std::decay
removes the const
and the reference attributes of a type.
As a result, if we pass lvalue references to std::make_tuple
, as we did in the above example, std::make_tuple
will store the corresponding decayed types. So in our example, std::make_tuple
creates a tuple of type std::tuple<int, std::string>
.
Then values_
, the data member of class X
, initialises all of its references (remember, it is a tuple of references) with the values inside of the unnamed, temporary tuple returned by std::make_tuple
.
But this unnamed, temporary tuple returned by std::make_tuple
gets destroyed at the end of the initialisation list of the constructor, leaving the references inside of values_
pointing to objects that no longer exist. Dereferencing those references therefore leads to undefined behaviour.
Note that there is an exception to the behaviour of std::make_tuple
when it determines the types to store inside the tuple: if some of the decayed type is std::reference_wrapper<T>
, then the tuple will have a T&
at the corresponding positions.
So we could, in theory, rewrite our example with std::ref
in order to create std::reference_wrapper
s:
#include <iostream> #include <functional> #include <tuple> template<typename... Ts> struct X { explicit X(Ts const&... values) : values_(std::make_tuple(std::ref(values)...)) {} std::tuple<Ts const&...> values_; }; int main() { int i = 42; auto s = std::string("universe"); auto x = X<int, std::string>(i, s); std::cout << "i = " << std::get<0>(x.values_) << '\n'; std::cout << "s = " << std::get<1>(x.values_) << '\n'; }
Now this program outputs what we wanted:
i = 42 s = universe
However, we shouldn’t use that, because there is a simpler solution: std::tie
.
std::tie
Like std::make_tuple
, std::tie
takes a variadic pack of parameters and creates a tuple out of them.
But unlike std::make_tuple
, std::tie
doesn’t std::decay
the types of its parameters. Quite the opposite in fact: it keeps lvalue references to its parameters!
So if we rewrite our example by using std::tie
instead of std::make_tuple
:
#include <iostream> #include <tuple> template<typename... Ts> struct X { explicit X(Ts const&... values) : values_(std::tie(values...)) {} std::tuple<Ts const&...> values_; }; int main() { int i = 42; auto s = std::string("universe"); auto x = X<int, std::string>(i, s); std::cout << "i = " << std::get<0>(x.values_) << '\n'; std::cout << "s = " << std::get<1>(x.values_) << '\n'; }
The we get the following output:
i = 42 s = universe
Which is what we want.
What happened is that std::tie
returned a tuple of references (of type std::tuple<int&, std::string&>
pointing to the arguments it received (i
and s
). values_
therefore also references those initial parameters.
std::forward_as_tuple
There is a third helper that takes a variadic pack of values and creates a tuple out of them: std::forward_as_tuple
.
To understand what it does and how it differs from std::make_tuple
and std::tie
, note that it has forward
in its name, just like std::forward
or like “forward” in “forwarding reference”.
std::forward_as_tuple
determines the types of the elements of the tuple like std::forward
does: if it receives an lvalue then it will have an lvalue reference, and if it receives an rvalue then it will have an rvalue reference (not sure about lvalues and rvalues in C++? Check out this refresher).
To illustrate, consider the following example:
#include <iostream> #include <tuple> #include <type_traits> std::string universe() { return "universe"; } int main() { int i = 42; auto myTuple = std::forward_as_tuple(i, universe()); static_assert(std::is_same_v<decltype(myTuple), std::tuple<int&, std::string&&>>); }
This program compiles (which implies that the static_assert
has its condition verified).
i
is an lvalue, universe()
is an rvalue, and the tuple returned by std::forward_as_tuple
contains a lvalue reference and an rvalue reference.
What should I use to build my tuple?
In summary, when you need to build a tuple, use:
std::make_tuple
if you need values in the returned tuple,std::tie
if you need lvalue references in the returned tuple,std::forward_as_tuple
if you need to keep the types of references of the inputs to build the tuple.
Make sure you choose the right one, otherwise you program might end up with dragons, clowns and butterflies.
You will also like
- STL Algorithms on Tuples
- The Demultiplexer Iterator: Routing Data to Any Numbers of Outputs
- Unzipping a Collection of Tuples with the unzip Smart Output Iterator
- Make your functions functional
Share this post!