Search code examples
c++stdtuplestd-tie

How does std::tie work?


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.


Solution

  • 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.