Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 431
user3188445 Avatar asked Jun 08 '21 07:06

user3188445


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

like image 157
StoryTeller - Unslander Monica Avatar answered Nov 20 '22 14:11

StoryTeller - Unslander Monica


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.

like image 7
Language Lawyer Avatar answered Nov 20 '22 14:11

Language Lawyer