Mati
Mati

Reputation: 781

Structured binding fails to compile

Consider following code snippet which introduces a custom Test type with support to structured binding:

struct Test {
    int member;
};

template<>
struct std::tuple_size<::Test>
{
    static constexpr size_t value = 1;
};

template<>
struct std::tuple_element<0, ::Test>
{
    using type = int;
};

template <size_t Index>
const int& get(const Test& test)
{
    // Omit Index for the sake of brevity
    return test.member;
}

int main() {
    Test test = Test{1234};
    auto [member] = test;
    return 0;
}

The code does not compile with following error:

error: binding reference of type ‘std::tuple_element<0, Test>::type&’ {aka ‘int&’} to ‘const int’ discards qualifiers

According to my understanding the structured binding should expand to (test is converted to xvalue and passed to get function):

In these initializer expressions, e is an lvalue if the type of the entity e is an lvalue reference (this only happens if the ref-qualifier is & or if it is && and the initializer expression is an lvalue) and an xvalue otherwise (this effectively performs a kind of perfect forwarding), i is a std::size_t prvalue, and is always interpreted as a template parameter list.

https://en.cppreference.com/w/cpp/language/structured_binding

Test test = Test{1234};
Test hidden = Test(test);
const int& member = get<0UL>(static_cast<Test &&>(hidden));

which compiles fine. However, it looks like the const qualifier is left out and the member's type is int& which for obvious reasons cannot compile and produce similar error message.

Upvotes: 3

Views: 117

Answers (2)

HolyBlackCat
HolyBlackCat

Reputation: 96286

Rather than const int &member, you get int &member:

cppreference

[the type is] reference to std::tuple_element<i, E>::type, ... lvalue reference if its corresponding initializer is an lvalue, rvalue reference otherwise

Where E is std::remove_reference_t<decltype((hidden))>, aka Test. And the "corresponding initializer" is what your get() returns.

std::tuple_element automatically propagates const from its template parameter to the resulting type, but it doesn't help, because E isn't const in the first place.


This boils down to get not being written correctly. You need 4 overloads, for all combinations of const, non-const, & and &&.

Those can be compacted to:

template <size_t Index, typename T>
requires std::is_same_v<std::remove_cvref_t<T>, Test>
auto &&get(T &&test)
{
    return std::forward<T>(test).member;
}

Alternatively, if you intend your type to be read-only when using tuple protocol, force tuple_element to always be const:

template<>
struct std::tuple_element<0, ::Test>
{
    using type = const int;
};

Upvotes: 5

user17732522
user17732522

Reputation: 76688

The reference-qualifier (lvalue vs rvalue) for the introduced variables are deduced from the initializer (i.e. your get overload), but the const-qualifications aren't. Except for the reference qualification, the whole type is retrieved from tuple_element, which in your case isn't const int, but just int.

The relevant rule in the standard is in sentence 8 of [dcl.struct.bind]. In your cppreference link it is mentioned above your quote:

For each identifier, a variable whose type is "reference to std::tuple_element<i, E>::type" is introduced: [...]

Upvotes: 1

Related Questions