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.
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.