8

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)?

Nimrod
  • 2,908
  • 9
  • 20
  • 1
    As for point 1: in your live demo the `Foo f` is const, in the code posted here - it's not. Please decide ;) – alagner Jun 06 '22 at 06:44
  • @alagner my bad. edited. Question is the same. – Nimrod Jun 06 '22 at 06:48
  • Care, [`decltype`](https://en.cppreference.com/w/cpp/language/decltype) has special rules for unparenthesis id-expression – Jarod42 Jun 06 '22 at 08:41
  • 1
    [More examples](https://godbolt.org/z/EPrsb7ooh) with parenthesis id-expression. – Jarod42 Jun 06 '22 at 08:47
  • @Jarod42 Thanks for your notificaiton. I believe the unparenthesis id-expression is just what I want, i.e. the type of the entity refered to. – Nimrod Jun 06 '22 at 09:19

2 Answers2

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.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • *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? – Nimrod Jun 28 '22 at 14:15
  • 1
    @Nimrod There is "_cv_" in front of it. That refers to the cv-qualifier introduced at the beginning of https://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 https://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`. – user17732522 Jun 28 '22 at 14:23
  • 1
    @Nimrod 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.) – user17732522 Jun 28 '22 at 14:30
  • Yes, you're right. But in my first case, where's cv? In the declaration, there are no cv-qualifiers at all! – Nimrod Jun 28 '22 at 14:30
  • @Nimrod Actually the entity issue had been resolved, see https://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 https://www.eel.is/c++draft/dcl.struct.bind#5.sentence-2. – user17732522 Jun 28 '22 at 14:37
  • @Nimrod The CWG issue changing it: https://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#2312 – user17732522 Jun 28 '22 at 14:39
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.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • 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.*? – Nimrod Jun 28 '22 at 14:25
  • I've seen the updated wording for it in C++20. – Nimrod Jun 28 '22 at 14:46
  • @Nimrod Just the [cpprefernce page](https://en.cppreference.com/w/cpp/language/structured_binding). I was thinking that something else changed in C++20 (in addition to what user17732522 linked about mutable members, but maybe not. – HolyBlackCat Jun 28 '22 at 15:10