7

I expected this code to work, but it does not compile:

#include <tuple>

struct S
{
    int x = 0;
    int y() const { return 1; }
};

bool f(const S& a, const S& b)
{
    return std::tie(a.x, a.y()) < std::tie(b.x, b.y());
}

GCC 9 says:

error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'

return std::tie(a.x, a.y()) < std::tie(b.x, b.y());
                     ~~~^~

What's wrong with the code, how can it be fixed, and why? I'm trying to write a concise comparison function, and usually std::tie supports that (indeed this is the textbook use case for std::tie).

Demo: https://godbolt.org/z/cWbQC0

John Zwinck
  • 239,568
  • 38
  • 324
  • 436

2 Answers2

8

std::tie always expects lvalues for arguments, since its intended purpose is to be used in assignment. To handle other value categories, one may use std::forward_as_tuple:

bool f(const S& a, const S& b)
{
    return std::forward_as_tuple(a.x, a.y()) < std::forward_as_tuple(b.x, b.y());
}

The two tuples now contain rvalue references that are bound to the results of the calls to S::y. Goes without saying that one best be careful with object lifetimes when using it.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • As I said to @jrok: Good solution, but then why does tie exist--is there some use case where tie is preferable to forward_as_tuple? It seems you can use forward_as_tuple instead of tie even for assignment. – John Zwinck Jul 05 '19 at 05:17
  • @JohnZwinck - Different purposes warrant different utilities. `tie` is for unpacking tuples into existing variables. Forwarding is for working with general callables. I also think the former is far more frequently done than the later, which is why it gets such a short name. The use we (yes me too) make of those utilities to get comparison for free is a bit hackish. – StoryTeller - Unslander Monica Jul 05 '19 at 07:26
  • That's quite unfortunate, as comparison operators are the second most common use case for `std::tie`. Given they came into existence around the same time, it's weird that `tie` isn't implemented as `forward_as_tuple` (keeping only the nice short name). – John Zwinck Jul 05 '19 at 08:24
7

std::tie takes lvalue references as arguments, so int returned by S::y can't bind. You could use the perfect forwarding version, std::forward_as_tuple, instead:

#include <tuple>

struct S
{
    int x = 0;
    int y() const { return 1; }
};

bool f(const S& a, const S& b)
{
    return std::forward_as_tuple(a.x, a.y()) < std::forward_as_tuple(b.x, b.y());
}

Demo.

jrok
  • 54,456
  • 9
  • 109
  • 141
  • 1
    Good solution, but then why does `tie` exist--is there some use case where `tie` is preferable to `forward_as_tuple`? – John Zwinck Jul 05 '19 at 05:17
  • `tie` is the usual case: `std::tie(a.x, a.y)` or especially `std::tie(a, b) = set.insert(x)`. `forward_as_tuple` is for a more general, more C++11-ish situation where you might have rvalue references and stuff. [`boost::tie`](https://www.boost.org/doc/libs/1_46_1/libs/tuple/doc/tuple_users_guide.html#tiers) (which became `std::tie`) pre-dates C++11 rvalue references. – Quuxplusone Jul 19 '23 at 13:39