Curious
Curious

Reputation: 21560

Initializer list and structured bindings deduction ambiguity in C++17

I had always avoided initializations like the following

const auto& x = a, y = b;
const int* const x = ptr_1, *const y = ptr_2;  // wot

For the reason that the reference and pointer qualifiers don't apply to both the initializations. Granted it's one of the first things beginners learn, the ambiguity associated with it makes me feel like the following is clearer and requires less thought on the reader's end

const auto& x = a;
const auto& y = b;

With C++17 and structured bindings I was happy and saw lots of potential. C++17 outlawed what C++14 and C++11 had failed to fix, auto x {1} is an int and not std::initializer_list<int>. But why does the following code not work?

const auto& [x, y] {a, b};
const auto& [x, y] = {a, b};

The latter is in line with the new rules for auto deduction and initializer lists, the expression on the right hand side is treated as an initializer list. But for the former compilation fails with the following error

initializer for variable '[a, b]' with type 'const auto &' contains multiple expressions

Is there any way I can declare both x and y with the structured bindings syntax without having to resort to tuples, pairs and the like? Also why is the former in the code example above ill formed code? Is there an ambiguity in that syntax?

Upvotes: 4

Views: 2946

Answers (2)

T.C.
T.C.

Reputation: 137404

Structured binding is, so to speak, for "unpacking" things. It's not designed to be a way to combine normal declarations. That const auto& applies to neither a nor b, despite the appearance.

Your particular attempt violates [dcl.dcl]/8:

A simple-declaration with an identifier-list is called a structured binding declaration ([dcl.struct.bind]). [...] 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.


int a = 1, b = 2;
const auto bitand <:x, y:> = std::tie(a, b);

This structured binding declaration is (very) roughly equivalent to

const auto bitand __e = std::tie(a, b); // hidden variable
auto and x = std::get<0>(__e);
auto and y = std::get<1>(__e);

(The real thing uses tuple_element, not auto.)

Notes:

  • The const auto bitand applies to the hidden variable and only the hidden variable. x and y are always references even if you write just auto; whether their referent is const depends on the const propagation properties of the initializer's type.
  • A temporary materialized from a prvalue initializer will have its lifetime extended by the reference binding.
  • In this example, both x and y are of type "reference to int"; it is valid to write x = 1;.
  • There's special treatment for structured bindings in the decltype wording.

These semantics are unsurprising if we are talking about unpacking a struct, etc., with two "reference to int" members; a const on such things doesn't actually affect the referent's constness. OTOH, you are in for a bad surprise if you want to use structured binding declarations for something they aren't designed to do.

Upvotes: 4

Henri Menke
Henri Menke

Reputation: 10939

This syntax is just not supported. You can only unpack aggregate classes and objects which for which std::get has been overloaded: https://skebanga.github.io/structured-bindings/

Unfortunately, you cannot really make use of the cool deduction guide because you want a reference to a and not to the tuple member. Thus you have to write out the template parameter list.

#include <tuple>

int main()
{
  int a = 1;
  int b = 2;
  const auto& [x, y] = std::tuple<int&,int&>{a, b};
}

You could also not be as stupid as me and read the docs correctly.

#include <tuple>

int main()
{
  int a = 1;
  int b = 2;
  const auto& [x, y] = std::forward_as_tuple(a, b);
}

const auto& [x, y] = std::tie(a, b); works as well.

Upvotes: 1

Related Questions