190

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.

Daniel Kamil Kozar
  • 18,476
  • 5
  • 50
  • 64
bolov
  • 72,283
  • 15
  • 145
  • 224

2 Answers2

222

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.

bolov
  • 72,283
  • 15
  • 145
  • 224
  • 2
    What bugs me is that there is that we're calling the assignment operator to an rvalue. – Adham Zahran Apr 22 '18 at 20:57
  • 2
    In [this](https://stackoverflow.com/a/4010943/661935) answer, it stated that a container can't hold a reference. Why `tuple` could hold a reference? – nn0p Aug 13 '19 at 02:14
  • 13
    @nn0p `std::tuple` is not a container, at least not in the C++ terminology, not the same as the `std::vector` and the likes. For instance you can't iterate with the usual ways over a tuple because it contains different types of objects. – bolov Aug 13 '19 at 04:32
  • 2
    @Adam tie(x,y) = make_pair(1,2); actually becomes std::tie(x, y).operator=(std::make_pair(1, 2)), it's why "assignment to an rvalue" works XD – Ju Piece Apr 23 '20 at 07:42
58

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
Damon
  • 67,688
  • 20
  • 135
  • 185
  • 2
    It's probably also worth mentioning that unlike `tie`, structured bindings can be used this way on types that aren't default-constructible. – Dan May 05 '17 at 09:15
  • 13
    Yeah, `std::tie()` is a lot less useful since C++17, where structured bindings are usually superior, but it still has uses, including assigning to existing (not simultaneously newly declared) variables and concisely doing other things like swapping multiple variables or other things that must assign to references. – underscore_d Nov 15 '18 at 15:36
  • 1
    Structured bindings can't be compared. Ties can. – TRPh Sep 02 '21 at 13:25
  • Thanks for this answer! I can now say good bye to std::get(...) calls forever! The call syntax is just so unnecessarily cumbersome. Who suggested it? – stackoverblown Oct 05 '22 at 12:51