Structured binding to replace std::tie abuse
Asked Answered
G

4

23

In reading this summary of the c++17 final features I was a bit surprised by the section on structured bindings (emphasis mine):

structured bindings

Until now, there was a known trick to abuse std::tie to assign a tuple or pair to different variables directly, instead of having to deal with the result type manually. This was a hack, and also the variables had to exist, now you can declare the variables and initialize them in one line:

auto [a , b , c] = getvalues();

The braces are needed, getvalues returns a tuple. std::pair is not mentioned in the proposal, so its unclear if this works with pair, which is returned by the STL in some insert methods.

I am assuming they refer to this kind of usage of std::tie

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

which I believed to be a recommended practice.

Can someone offer an explanation as to why they are referring to above example as a hack?

Glossotomy answered 25/10, 2016 at 13:29 Comment(1)
The comma splices in that quoted text are unbearable!Enjoy
T
45

I can put it simply like that:

In a language where functions can return just one variable

int a,b,c;
std::tie(a,b,c) = function_returning_multiple_values();

is a hack for:

auto [a, b, c] = function_returning_multiple_values();

just as in the hypothetical world where C++ would allow just one parameter for functions

int p1, p2, p3;
p1 = ...;
p2 = ...;
p3 = ...;

function_taking_multiple_params(std::tie_params(p1, p2, p3));

would be a hack for:

function_taking_multiple_params(p1, p2, p3)

You are so accustomed with the C++ restriction that a function can return at most one object, but in fact it is just an artificial language restriction, just as a restriction to accept at most one parameter would be an artificial language restriction.

The std::tie is a library hack for a missing language feature. And it has some drawbacks:

  • the variables need be declared beforehand
  • the variable types must be declared explicitly
  • Inefficient or can't be used with types that are not default constructible

Are structured bindings everything that they could have been? No, but for the most cases they are everything we need.

What is missing?

  • Explicit type for some elements: e.g.:
auto [a, std::string b, c] = foo();

where a and c have the type deduced and b is explicit "std::string"

  • Nesting. E.g.:
auto [a, [b1, b2], c] = foo();

where the second returned object from foo is a tuple like object.

  • Language feature at the return site (bypassing std::tuple all together):
auto foo() -> [int, int]

instead of

auto foo() -> std::tuple<int, int>
  • Named return objects
auto foo() -> [int& key, int& value]

... well... wouldn't that be nice

  • and combine that with... - get ready for a cool new name - Generalized return initialization:
auto minmax_element(It begin, It end) -> [It min_it, It max_it];

auto [min = *min_it, max = *max_it] = minmax_element(...);
Thirlage answered 25/10, 2016 at 13:37 Comment(8)
What would it be the signature of function_returning_multiple_values() ?Glossotomy
@EmeraldWeapon: It would return some object that structured binding operates on. For the std::tie example, it would be returning a std::tuple of types.Ferruginous
Ok, so there still won't be any true multiple return value in c++17?Glossotomy
@EmeraldWeapon: Structured binding is probably the best C++ is going to get.Ferruginous
Ok, thanks for sharing your perspectives. This leaves me the feeling that the statement 'is a hack' is maybe a bit of an over-emphasis. I guess one may say that std::tie is just syntactic sugar for structure unpacking, or a mock-up for a missing language feature.Glossotomy
@EmeraldWeapon You are welcome. I see it as a hack. It's not syntactic sugar because 1. unpacking couldn't be done otherwise 2. Has drawbacks. That's how I see a syntactic sugar. For instance you can say ranged for is a syntactic sugar. Adds a new syntactic feature that lets you in less words and in a clearer way do what could have been done previously with a lousier syntax. There are things that you simply cannot do without structured bindings. E.g. initialize a non default constructible object, or a reference.Thirlage
Also "decomposition operator" often desired for unpacking a struct into a list of a function arguments (saving the r/l-valueness of a compound passed to this imaginary operator). Variadic abilities are also highly desired.Contaminate
Another thing that is missing is that sometimes you want to declare new names in a scope, but sometimes you actually WANT to assign to an existing variable (or perhaps a mix of existing and new). The fancy new syntax only declares new variables and std::tie only uses existing variables. A mix is useful in cases like the return of std::map::emplace where you probably already have an iterator variable but not a bool declared.Chanty
S
8

A very noticeable difference is std::ignore. Look at the example

std::tuple<string, string> data {"Lord", "Buddha"};
auto [a, b] = data; //valid
auto [ , b] = data; //not valid as the identifier is strongly required
string y;
std::tie( std::ignore, y ) = data; //voila
Surah answered 18/6, 2020 at 12:8 Comment(0)
C
5

std::tie in itself has another functionality.

It was meant for creating a tuple with references to variables

Creates a tuple of lvalue references to its arguments or instances of std::ignore.

This is useful for creating on-the-fly tuples without having to copy the variables because they are references. I just take the example from cppreference for a usecase.

bool operator<(const S& rhs) const
{
    // compares n to rhs.n,
    // then s to rhs.s,
    // then d to rhs.d
    return std::tie(n, s, d) < std::tie(rhs.n, rhs.s, rhs.d);
}

Here tuples are created but they don't copy the variables but have references.

Now because they hold references you could "hack" it to do something like this

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

It assigns the values of the returned tuple to the one with the references in itself.

This is even on cpprefence just mentioned as a "note"

std::tie may be used to unpack a std::pair because std::tuple has a converting assignment from pairs

In c++17 they introduced "structured bindings" to take care of the scenario to assign multible variables at once. So whether it was intentional or a hack, since c++17 this usage of tie should not be necessary anymore.

Whether std::tie was meant to be used that way or is a "hack" may be personal opinion, I guess the people who introduced std::tie know best for this. But considering how structured binding kind of replaces std::tie in that instance, they came up with a solution they think is better.

Conspire answered 25/10, 2016 at 13:56 Comment(6)
Boost.Tuple shows tuple decomposition as the primary example for boost::tie. Indeed, it is the only example for the use of boost::tie.Ferruginous
@NicolBolas Well this was about std::tie and more why it is considered a "hack" in the referred article. This in an explanation why people may think that way. I also put "hack" in quotation marks to indicate that this may not be seen as a hack but some may see this as the intention. I will edit this though to the answer.Conspire
If this was not an intended use for std::tie, what was the intended purpose of std::ignore?Plerre
@Plerre that's true that ignore is only for the unpacking functionality. Considering how that was introduced with 11 while maybe it was intentional back then it could have been considered as a "temporary hack" until structured bindings are out. As mentioned: the interpretation whether it was a hack or not basically belongs to the original article and I tried to provide some reason why it may have been described that way.Conspire
@Conspire then perhaps reconsider this sentence? "So in c++11 was not really an official way to directly assign values but std::tie can be used as this but maybe was never intended to be used that way."Plerre
@Plerre well the whole point of that sentence was to explain how this usecase of "tie" was basically later covered by structured bindings. I tried to rephrase that nowConspire
L
2

I'm hoping nobody minds me bringing my opinion to the mix because I think it's still valid. I agree with a lot of what is said, but I don't think structured bindings will replace std::tie. There is a specific use-case for std::tie that you simply can't do with structured bindings because the variables are declared at site. Now don't get me wrong, I'm a fan of structured bindings, but I recently came across a case where they just just didn't cut it. I had this structure...

std::vector<std::tuple<std::string, uint32_t, uint64_t>> values;

typedef struct
{
    std::string s;
    uint32_t o;
    uint64_t v;
} A;

std::vector<A> my_objs;

Okay, so I had vector of tuples and a vector of objects and what I want to do was to take the values from the tuples and assign these values to each of the existing objects in the vector as below:

// (This is a very contrived example and you should assume that the
// objects in my_obj are much more complex, already exist and you just want
// to set some values in them)

for (size_t i = 0; i < my_obj.size(); i++)
    std::tie(my_objs.at(i).s, my_objs.at(i).o, my_objs.at(i).v) = values.at(i);

// Sure, you could create a method for my_obj that takes the values, but that's a very
// heavy handed approach and missing the point.

If the variable doesn't exist, then structured bindings are your best friend, but if it does, they simply don't help. Also, as someone has mentioned, there are a number of other omissions from structured bindings that for me, means they come up lacking. Firstly the ability to nest them, such that one of the variables might itself be a tuple/pair etc. Secondly, although this is a little different, the inability to use structured bindings in lambda expression declarators like this:

std::unordered_map<std::string, Item*> items;

std::for_each(items.begin(), items.end(), [](const auto&[s, item]) { delete item; });   // Not allowed, you have to do...
std::for_each(items.begin(), items.end(), [](const auto& item_pair) { delete item_pair.second; });   // Acceptable

What I think would have been useful for structured bindings would be to have the ability that the declared variables could be references to existing objects. So although I feel that some people think that std::tie will be replaced by structured bindings, the reality is that std::tie still serves a very useful purpose that structured bindings could deliver, but just doesn't.

Lavalava answered 29/1, 2021 at 7:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.