Reputation: 781
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
Reputation: 96286
Rather than const int &member
, you get int &member
:
[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
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