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

Viewed 748

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.

3 Answers

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.

The root of your question seems to be the difference between decltype(X::i) and decltype((X::i)). Why does (X::i) yield a int&? See:

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

otherwise, if E is an lvalue, decltype(E) is T&, where T is the type of E;

However, the key point here is that the fact that it yields a T& doesn't really matter:

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

If an expression initially has the type “reference to T” ([dcl.ref], [dcl.init.ref]), the type is adjusted to T prior to any further analysis. The expression designates the object or function denoted by the reference, and the expression is an lvalue or an xvalue, depending on the expression. [ Note: Before the lifetime of the reference has started or after it has ended, the behavior is undefined (see [basic.life]). — end note ]

What "justifies" it then? Well, when doing decltype(X::i) we're concerned primarily with what the type of X::i is and not its value category or its properties when treated as an expression. However, (X::i) is there if we do care.

Related