Search code examples
c++c++17structured-bindings

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


Why are structured bindings defined through a uniquely named variable and all the vague "name is bound to" language?

I personally thought structured bindings worked as follows. Given a struct:

struct Bla
{
    int i;
    short& s;
    double* d;
} bla;

The following:

cv-auto ref-operator [a, b, c] = bla;

is (roughly) equivalent to

cv-auto ref-operator a = bla.i;
cv-auto ref-operator b = bla.s;
cv-auto ref-operator c = bla.d;

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.

So I'm clearly missing something, but what is the exact case where a well-defined expansion in the sense of, let's say, fold expressions, which a lot simpler to read up on in standardese?

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.


Solution

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