Search code examples
c++language-lawyerc++20decltype

What justifies the lvalue category of unevaluated non-static data members in C++?


Both gcc and clang accept the following code, and I'm trying to figure out why.

// c++ -std=c++20 -Wall -c test.cc

#include <concepts>

struct X {
  int i;
};

// This is clearly required by the language spec:
static_assert(std::same_as<decltype(X::i), int>);

// This seems more arbitrary:
static_assert(std::same_as<decltype((X::i)), int&>);

The first static_assert line makes sense according to [dcl.type.decltype]:

otherwise, if E is an unparenthesized id-expression or an unparenthesized class member access ([expr.ref]), decltype(E) is the type of the entity named by E. If there is no such entity, or if E names a set of overloaded functions, the program is ill-formed;

- https://timsong-cpp.github.io/cppwp/n4861/dcl.type.decltype#1.3

X::i is a valid id-expression in an unevaluated context, so its decltype should be the declared type of i in X.

The second static_assert has me stumped. There's only one clause in [dcl.type.decltype] in which a parenthesized expression yields an lvalue reference: it must be that both compilers consider X::i to be an expression of lvalue category. But I can't find any support for this in the language spec.

Obviously if i were a static data member then X::i would be an lvalue. But for a non-static member, the only hint I can find is some non-normative language in [expr.context]:

In some contexts, unevaluated operands appear ([expr.prim.req], [expr.typeid], [expr.sizeof], [expr.unary.noexcept], [dcl.type.simple], [temp.pre], [temp.concept]). An unevaluated operand is not evaluated. [ Note: In an unevaluated operand, a non-static class member may be named ([expr.prim.id]) and naming of objects or functions does not, by itself, require that a definition be provided ([basic.def.odr]). An unevaluated operand is considered a full-expression. — end note ]

- https://timsong-cpp.github.io/cppwp/n4861/expr.prop#expr.context-1

This suggests decltype((X::i)) is a valid type, but the definition of full-expression doesn't say anything about value categories. I don't see what justifies int& any more than int (or int&&). I mean an lvalue is a glvalue, and a glvalue is "an expression whose evaluation determines the identity of an object." How can an expression like X::i--which can't be evaluated at all, let alone determine the identity an object--be considered a glvalue?

Are gcc and clang right to accept this code, and if so what part of the language specification supports it?

remark: StoryTeller - Unslander Monica's answer makes even more sense in light of the fact that sizeof(X::i) is allowed and that decltype((X::i + 42)) is a prvalue.


Solution

  • How can an expression like `X::i--which can't be evaluated at all, let alone determine the identity an object--be considered a glvalue?

    Ignoring the misuse of «result», it is [expr.prim.id.qual]/2:

    A nested-name-specifier that denotes a class, optionally followed by the keyword template ([temp.names]), and then followed by the name of a member of either that class ([class.mem]) or one of its base classes, is a qualified-id; ... The result is an lvalue if the member is a static member function or a data member and a prvalue otherwise.

    The value category of an expression is not determined by the «definition» in [basic.lval], like «an expression whose evaluation determines the identity of an object», but specified for each kind of expression explicitly.