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?
That's not entirely true. That expression can be evaluated (following a suitable transformation in the right context).
[class.mfct.non-static]
3 When an id-expression that is not part of a class member access syntax and not used to form a pointer to member ([expr.unary.op]) is used in a member of class X in a context where this can be used, if name lookup resolves the name in the id-expression to a non-static non-type member of some class C, and if either the id-expression is potentially evaluated or C is X or a base class of X, the id-expression is transformed into a class member access expression using (*this) as the postfix-expression to the left of the . operator.
So we can have something like
struct X {
int i;
auto foo() const { return X::i; }
};
Where X::i
is transformed into (*this).X::i
. Which is explicitly allowed on account of
[expr.prim.id]
2 An id-expression that denotes a non-static data member or non-static member function of a class can only be used:
- as part of a class member access in which the object expression refers to the member's class or a class derived from that class, or ...
And that meaning of (*this).X::i
is always an lvalue denoting the class member i
.
So you see, in the contexts where X::i
can be evaluated, it always produces an lvalue. So decltype((X::i))
in those context would need to be an lvalue reference type. And while outside of class scope X::i
cannot generally be used in an expression, it's still not entirely arbitrary to say its value category is an lvalue. We are just expanding the region of definition a bit (which doesn't contradict the standard, since the standard doesn't define it).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With