What Are Use Cases for Structured Bindings

What are use cases for structured bindings?

Can you provide some other, possibly less obvious use cases for structured bindings? How else can they improve readability or even performance of C++ code?

More in general, you can use it to (let me say) unpack a structure and fill a set of variables out of it:

struct S { int x = 0; int y = 1; };

int main() {
S s{};
auto [ x, y ] = s;
(void)x, void(y);
}

The other way around would have been:

struct S { int x = 0; int y = 1; };

int main() {
S s{};
auto x = s.x;
auto y = s.y;
(void)x, void(y);
}

The same is possible with arrays:

int main() {
const int a[2] = { 0, 1 };
auto [ x, y ] = a;
(void)x, void(y);
}

Anyway, for it works also when you return the structure or the array from a function, probably you can argue that these examples belong to the same set of cases you already mentioned.


Another good example mentioned in the comments to the answer by @TobiasRibizel is the possibility to iterate through containers and unpack easily the contents.

As an example based on std::map:

#include <map>
#include <iostream>

int main() {
std::map<int, int> m = {{ 0, 1 }, { 2, 3 }};
for(auto &[key, value]: m) {
std::cout << key << ": " << value << std::endl;
}
}

What is the best practice for performing structured bindings on read-only (i.e. const) accessors?

So my question is: which one of the above is considered a better practice?

Prefer second method to try to mimic what happens with built-in and standard:

class foo {
public:
const int x = 0;
const int y = 0;
};

int main() {
foo f;
auto [a, b] = f; // Compiles
auto& [x, y] = f; // Compiles
const auto [c_a, c_b] = f; // Compiles
const auto& [c_x, c_y] = f; // Compiles
}

And from binding_a_tuple-like_type

std::tuple<float&,char&&,int> tpl(x,std::move(y),z);
const auto& [a,b,c] = tpl;
// a names a structured binding that refers to x; decltype(a) is float&
// b names a structured binding that refers to y; decltype(b) is char&&
// c names a structured binding that refers to the 3rd element of tpl; decltype(c) is const int

Why are structured bindings defined in terms of a uniquely named variable?

It seems all the other behaviour of the variables defined by a structured binding actually follow the as-if simple expansion "rule" I'd think would be used to define the concept.

It kind of does. Except the expansion isn't based on the expression on the right hand side, it's based on the introduced variable. This is actually pretty important:

X foo() {
/* a lot of really expensive work here */
return {a, b, c};
}

auto&& [a, b, c] = foo();

If that expanded into:

// note, this isn't actually auto&&, but for the purposes of this example, let's simplify
auto&& a = foo().a;
auto&& b = foo().b;
auto&& c = foo().c;

It wouldn't just be extremely inefficient, it could also be actively wrong in many cases. For instance, imagine if foo() was implemented as:

X foo() {
X x;
std::cin >> x.a >> x.b >> x.c;
return x;
}

So instead, it expands into:

auto&& e = foo();
auto&& a = e.a;
auto&& b = e.b;
auto&& c = e.c;

which is really the only way to ensure that all of our bindings come from the same object without any extra overhead.

And the equivalent expansions for arrays and tuples. But apparently, that would be too simple and there's all this vague special language used to describe what needs to happen.

There's three cases:

  1. Arrays. Each binding acts as if it's an access into the appropriate index.
  2. Tuple-like. Each binding comes from a call to std::get<I>.
  3. Aggregate-like. Each binding names a member.

That's not too bad? Hypothetically, #1 and #2 could be combined (could add the tuple machinery to raw arrays), but then it's potentially more efficient not to do this.

A healthy amount of the complexity in the wording (IMO) comes from dealing with the value categories. But you'd need that regardless of the way anything else is specified.

Do structured bindings and forwarding references mix well?

Yes. Structured bindings and forwarding references mix well.

In general, any place you can use auto, you can use auto&& to acquire the different meaning. For structured bindings specifically, this comes from [dcl.struct.bind]:

Otherwise, e is defined as-if by

attribute-specifier-seqopt decl-specifier-seq ref-qualifieropt e initializer ;

where the declaration is never interpreted as a function declaration and the parts of the declaration other than the declarator-id are taken from the corresponding structured binding declaration.

There are further restrictions on these sections in [dcl.dcl]:

A simple-declaration with an identifier-list is called a structured binding declaration ([dcl.struct.bind]). The decl-specifier-seq shall contain only the type-specifier auto and cv-qualifiers. The initializer shall be of the form “= assignment-expression”, of the form “{ assignment-expression }”, or of the form “( assignment-expression )”, where the assignment-expression is of array or non-union class type.

Putting it together, we can break down your example:

auto&& [bla, blabla] = something();

as declaring this unnamed variable:

auto               && e = something();
~~~~ ~~ ~~~~~~~~~~~
decl-specifier-seq initializer
ref-qualifier

The behavior is that is derived from [dcl.spec.auto] (specifically here). There, we do do deduction against the initializer:

template <typename U> void f(U&& );
f(something());

where the auto was replaced by U, and the && carries over. Here's our forwarding reference. If deduction fails (which it could only if something() was void), our declaration is ill-formed. If it succeeds, we grab the deduced U and treat our declaration as if it were:

U&& e = something();

Which makes e an lvalue or rvalue reference, that is const qualified for not, based on the value category and type of something().

The rest of the structured bindings rules follow in [dcl.struct.bind], based on the underlying type of e, whether or not something() is an lvalue, and whether or not e is an lvalue reference.


With one caveat. For a structured binding, decltype(e) always is the referenced type, not the type you might expect it be. For instance:

template <typename F, typename Tuple>
void apply1(F&& f, Tuple&& tuple) {
auto&& [a] = std::forward<Tuple>(tuple);
std::forward<F>(f)(std::forward<decltype(a)>(a));
}

void foo(int&&);

std::tuple<int> t(42);
apply1(foo, t); // this works!

I pass my tuple is an lvalue, which you'd expect to pass its underlying elements in as lvalue references, but they actually get forwarded. This is because decltype(a) is just int (the referenced type), and not int& (the meaningful way in which a behaves). Something to keep in mind.

There are two places I can think of where this is not the case.

In trailing-return-type declarations, you must use just auto. You can't write, e.g.:

auto&& foo() -> decltype(...);

The only other place I can think of where this might not be the case is part of the Concepts TS where you can use auto in more places to deduce/constrain types. There, using a forwarding reference when the type you're deducing isn't a reference type would be ill-formed I think:

std::vector<int> foo();
std::vector<auto> a = foo(); // ok, a is a vector<int>
std::vector<auto&&> b = foo(); // error, int doesn't match auto&&

Can the structured bindings syntax be used in polymorphic lambdas

This is not currently allowed by the syntax; structured bindings are a simple-declaration:

simple-declaration:[...]

- attribute-specifier-seqopt decl-specifier-seq ref-qualifieropt [ identifier-list ] initializer ;

while function parameters are introduced by a parameter-declaration-list, which contains declarators:

The declarators specify the names of these entities and (optionally) modify the type of the specifiers with operators such as * (pointer to) and () (function returning).

That is, a structured binding is a (block-level) statement syntax - you can see this by noting that the grammar for it ends in a semicolon ;. Allowing structured bindings in a lambda parameter list would require additional grammar to be added.

It sounds like a good idea, and I can't immediately see any ambiguity in the syntax; it would certainly be worth discussing as it solves your presented use case nicely and more concisely than the alternatives.

Structured bindings in foreach loop

As said by user17732522, you can use range-based for loops for this purpose as such:

#include <iostream>
#include <vector>

using trading_day = std::pair<int, bool>;
using fun_intersection = std::vector<std::pair<int, bool>>;

int main()
{
fun_intersection fi({ {1, true}, {0, true}, {1, false}, {0, false} });
for (auto& [day, action] : fi)
{
if (day == 1 && action == true) std::cout << "Success!" << std::endl;
else std::cout << "Fail!" << std::endl;
}
}

Output:

Success!
Fail!
Fail!
Fail!

Structured bindings in Python

Yes, you can use __iter__ method since iterators can be unpacked too:

class pair:
def __init__(self, first, second):
self.first = first
self.second = second
def __iter__(self):
# Use tuple's iterator since it is the closest to our use case.
return iter((self.first, self.second))

p = pair(1, 2)
a, b = p
print(a, b) # Prints 1 2

Are there features (in particular, structured binding) which I can't use without 'auto'?

Am I correct that there's no way to initialize [x, y] without using
auto (still using structured bindings)?

Yes, this is correct. The grammar specifies no other way, as can be seen for example here.

What other features require using auto?

A classical example should be the (generic) lambda expressions:

auto lambda = [](auto&&...) { };

But as stated in the comments, there are some other examples as well.



Related Topics



Leave a reply



Submit