How does std::tie work?

In order to clarify the core concept, let's reduce it to a more basic example. Although std::tie is useful for functions returning (a tuple of) more values, we can understand it just fine with just one value:

int a;
std::tie(a) = std::make_tuple(24);
return a; // 24

Things we need to know in order to go forward:

  • std::tie constructs and returns a tuple of references.
  • std::tuple<int> and std::tuple<int&> are 2 completely different classes, with no connection between them, other that they were generated from the same template, std::tuple.
  • tuple has an operator= accepting a tuple of different types (but same number), where each member is assigned individually—from cppreference:

    template< class... UTypes >
    tuple& operator=( const tuple<UTypes...>& other );
    

    (3) For all i, assigns std::get<i>(other) to std::get<i>(*this).

The next step is to get rid of those functions that only get in your way, so we can transform our code to this:

int a;
std::tuple<int&>{a} = std::tuple<int>{24};
return a; // 24

The next step is to see exactly what happens inside those structures. For this, I create 2 types T substituent for std::tuple<int> and Tr substituent std::tuple<int&>, stripped down to the bare minimum for our operations:

struct T { // substituent for std::tuple<int>
    int x;
};

struct Tr { // substituent for std::tuple<int&>
    int& xr;

    auto operator=(const T& other)
    {
       // std::get<I>(*this) = std::get<I>(other);
       xr = other.x;
    }
};

auto foo()
{
    int a;
    Tr{a} = T{24};

    return a; // 24
}

And finally, I like to get rid of the structures all together (well, it's not 100% equivalent, but it's close enough for us, and explicit enough to allow it):

auto foo()
{
    int a;

    { // block substituent for temporary variables

    // Tr{a}
    int& tr_xr = a;

    // T{24}
    int t_x = 24;

    // = (asignement)
    tr_xr = t_x;
    }

    return a; // 24
}

So basically, std::tie(a) initializes a data member reference to a. std::tuple<int>(24) creates a data member with value 24, and the assignment assigns 24 to the data member reference in the first structure. But since that data member is a reference bound to a, that basically assigns 24 to a.


This does not answer your question in any way, but let me post it anyway because C++17 is basically ready (with compiler support), so while wondering how the outdated stuff works, it is probably worth looking at how the current, and future, version of C++ works, too.

With C++17 you can pretty much scratch std::tie in favour of what is called structured bindings. They do the same (well, not the same, but they have the same net effect), although you need to type fewer characters, it does not need library support, and you also have the ability to take references, if that happens to be what you want.

(Note that in C++17 constructors do argument deduction, so make_tuple has become somewhat superfluous, too.)

int a, b;
std::tie(a, b) = std::make_tuple(2, 3);

// C++17
auto  [c, d] = std::make_tuple(4, 5);
auto  [e, f] = std::tuple(6, 7);
std::tuple t(8,9); auto& [g, h] = t; // not possible with std::tie

Tags:

C++

Tuples

C++11