cv-qualifier propagation in structured binding
Asked Answered
F

2

7

As quoted in dcl.struct.bind,

Let cv denote the cv-qualifiers in the decl-specifier-seq.

Designating the non-static data members of E as m 0 , m 1 , m 2 , ... (in declaration order), each v i is the name of an lvalue that refers to the member m i of e and whose type is cv T i , where T i is the declared type of that member;

If I'm understanding correctly, the cv-qualifiers are propagated from the declartion of structured binding.

Say I have a simple struct,

struct Foo {
    int x;
    double y;
};

Consider the two scenarios,

const Foo f{1, 1.0};
auto& [x, y] = f;
// static_assert(std::is_same<decltype(x), int>::value); // Fails!
static_assert(std::is_same<decltype(x), const int>::value); // Succeeds

Live Demo. Does the cv-qualifier of x come from the deduction auto?

The second one,

Foo f{1, 1.0};

const auto& [x, y] = f;
const auto& rf = f;

static_assert(std::is_same<decltype(x), const int>::value); // with const
static_assert(std::is_same<decltype(rf.x), int>::value); // without const

Live Demo. The result complies with the standard, which makes sense.

My second question is is there any reason to propagate the cv-qualifiers, isn't it a kind of inconsistent (to the initialization a reference with auto)?

Fusain answered 6/6, 2022 at 4:30 Comment(5)
As for point 1: in your live demo the Foo f is const, in the code posted here - it's not. Please decide ;)Handset
@Handset my bad. edited. Question is the same.Fusain
Care, decltype has special rules for unparenthesis id-expressionOrcein
More examples with parenthesis id-expression.Orcein
@Orcein Thanks for your notificaiton. I believe the unparenthesis id-expression is just what I want, i.e. the type of the entity refered to.Fusain
H
3

decltype has a special rule when the member of a class is named directly as an unparenthesized member access expression. Instead of producing the result it would usually if the expression was treated as an expression, it will result in the declared type of the member.

So decltype(rf.x) gives int, because x is declared as int. You can force decltype to behave as it would for other expressions by putting extra parentheses (decltype((rf.x))), in which case it will give const int& since it is an lvalue expression and an access through a const reference.

Similarly there are special rules for decltype if a structured binding is named directly (without parentheses), which is why you don't get const int& for decltype(x).

However the rules for structured bindings take the type from the member access expression as an expression if the member is not a reference type, which is why const is propagated. At least that is the case since the post-C++20 resolution of CWG issue 2312 which intends to make the const propagation work correctly with mutable members.

Before the resolution the type of the structured binding was actually specified to just be the declared type of the member with the cv-qualifiers of the structured binding declaration added, as you are quoting in your question.

I might be missing some detail on what declared type refers to exactly, but it seems to me that this didn't actually specify x to have type const int& in your first snippet (and decltype hence also not const), although that seems to be how all compilers always handled that case and is also the only behavior that makes sense. Maybe it was another defect, silently or unintentionally fixed by CWG 2312.

So, practically speaking, both rf.x and x in your example are const int lvalue expressions when you use them as expressions. The only oddity here is in how decltype behaves.

Helprin answered 28/6, 2022 at 11:34 Comment(6)
which is why you don't get const int& for decltype(x). I'm not expecting to get const int& but int in both cases. each vi ... whose type is cv Ti from the standard, here vi should refer to the entity but not the expression right?Fusain
@Fusain There is "cv" in front of it. That refers to the cv-qualifier introduced at the beginning of timsong-cpp.github.io/cppwp/n4659/dcl.struct.bind#1: "Let cv denote the cv-qualifiers in the decl-specifier-seq.", meaning that the cv-qualifiers of the structured binding declaration are added. But the important part is actually the part after that: "the referenced type is cv Ti". Because timsong-cpp.github.io/cppwp/n4659/dcl.type.simple#4.1 says that decltype produces the referenced type. Also note that the wording changed since C++20 to fix an issue with mutable.Helprin
@Fusain And yes, that sentence specifies the type of the entity vi. Ti itself could be a reference type, but expressions don't have reference types. (Although technically I guess the structured binding doesn't actually introduce an entity for vi. It is just a name and the paragraph describes the expression type and value category that vi has as an expression. In that sense the standard might have some minor wording issue here.)Helprin
Yes, you're right. But in my first case, where's cv? In the declaration, there are no cv-qualifiers at all!Fusain
@Fusain Actually the entity issue had been resolved, see github.com/cplusplus/draft/pull/1884. But you are right, following the C++20 draft wording I also don't see where the const comes from in your first snippet. However, since C++20 the wording was changed and now it talks not about the declared type in case of non-references, but about the type of the expression e.mi, in which case that does have the const. See eel.is/c++draft/dcl.struct.bind#5.sentence-2.Helprin
@Fusain The CWG issue changing it: open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#2312Helprin
G
2

The cvref-qualifiers don't always propagate. They apply only to the single hidden variable that stores a copy/reference to the initializer.

Ref-qualifiers don't affect the identifiers created by the binding, those always act as references to the said common hidden variable. But if that variable itself is a copy, this causes them to behave almost like they were true copies.

How cv-qualifiers propagate is affected by what you're binding:

  • For non-tuple-like classes and arrays, the behavior is defined in terms of . and [] respectively, which normally propagate const, except for mutable class members.

  • For tuple-like classes, it depends on how get<I>() is implemented for your type. For tuples and similar classes, it propagates const only if the element is not a reference.

    E.g. for a tuple of non-const references, the binding will always produce non-const identifiers.


The inconsistency you're observing for decltype(x) vs decltype(rf.x) is caused by both of them being different special cases for decltype. Arguably the former makes more sense, but it's too late to change the latter.

If you add a second pair of parentheses, you'll get the same behavior for both.

Gama answered 28/6, 2022 at 11:38 Comment(3)
Thanks. It kinda makes sense to me. Could you provide more references to For non-tuple-like classes and arrays, the behavior is defined in terms of . and [] respectively, which normally propagate const, except for mutable class members.?Fusain
I've seen the updated wording for it in C++20.Fusain
@Fusain Just the cpprefernce page. I was thinking that something else changed in C++20 (in addition to what user17732522 linked about mutable members, but maybe not.Gama

© 2022 - 2024 — McMap. All rights reserved.