How does std::tie work?

bolov picture bolov · May 3, 2017 · Viewed 36.9k times · Source

I've used std::tie without giving much thought into it. It works so I've just accepted that:

auto test()
{
   int a, b;
   std::tie(a, b) = std::make_tuple(2, 3);
   // a is now 2, b is now 3
   return a + b; // 5
}

But how does this black magic work? How does a temporary created by std::tie change a and b? I find this more interesting since it's a library feature, not a language feature, so surely it is something we can implement ourselves and understand.

Answer

bolov picture bolov · May 3, 2017

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.