Is lvalue to rvalue conversion not applied on empty class object passed by value
Asked Answered
R

2

4

I wrote the following program where if a class have a non-static data member then when an object of that type is passed as argument by value, then it can't be used in a constexpr context. After searching for the issue on the web, I came across this gcc bug where some user said something like when the class is empty, then no lvalue to rvalue transformation is applied which is why the A in the below example works but not B. But then other user is saying that this isn't the case.

I want to know how exactly the standard allows the empty class case to work but not the non-empty class case. Demo

struct A {
    static constexpr int value = 42;
};
struct B
{
    static constexpr int value = 42;
    int mem{};
};

constexpr int f(A a) { return A::value; }
constexpr int f(B b) { return B::value; } 

int main() {
    A a; 
    constexpr int aconst = f(a); //works in all compilers

    B b;
    constexpr int bconst = f(b); //fails in all compilers, why?
}

Gcc says:

<source>: In function 'int main()':
<source>:18:31: error: the value of 'b' is not usable in a constant expression
   18 |     constexpr int bconst = f(b); //fails in all compilers, why?
      |                               ^
<source>:17:7: note: 'b' was not declared 'constexpr'
   17 |     B b;
      |       

Basically I want to know if the claim that lvalue to rvalue transformation is bypassed in case of empty class case is true or not. And if it is, where/how exactly according to the standard.

Riotous answered 3/3 at 9:24 Comment(1)
There's a general rule that a constexpr function can run at compile-time even if it has an non-constexpr reference parameter, as long as it's unused. And this is just a manifestation of that, apparently. (Here the function is the copy constructor.)Candler
M
2

[dcl.init.general]/14

The initialization that occurs [...] as well as in argument passing [...] is called copy-initialization.

The semantics of the copy-initializations in the two function calls in the question are governed by [dcl.init.general]/16.6.2:

Otherwise, if the destination type is a (possibly cv-qualified) class type:

  • [...]

  • Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated ([over.match.ctor]), and the best one is chosen through overload resolution ([over.match]). Then:

    • If overload resolution is successful, the selected constructor is called to initialize the object, with the initializer expression or expression-list as its argument(s).
    • [...]
  • [...]

The initialization of the parameter a from the argument a, or the parameter b from the argument b, is by constructor. In these cases the copy constructors are used (I hope this is obvious enough that I don't need to explain it).

Since A and B don't have user-declared copy constructors, [class.copy.ctor]/6 applies:

If the class definition does not explicitly declare a copy constructor, a non-explicit one is declared implicitly. If the class definition declares a move constructor or move assignment operator, the implicitly declared copy constructor is defined as deleted; otherwise, it is defaulted ([dcl.fct.def]). The latter case is deprecated if the class has a user-declared copy assignment operator or a user-declared destructor ([depr.impldec]).

The semantics of an implicitly declared copy constructor are given by [class.copy.ctor]/14.

The implicitly-defined copy/move constructor for a non-union class X performs a memberwise copy/move of its bases and members. [...] The order of initialization is the same as the order of initialization of bases and members in a user-defined constructor (see [class.base.init]). Let x be either the parameter of the constructor or, for the move constructor, an xvalue referring to the parameter. Each base or non-static data member is copied/moved in the manner appropriate to its type:

  • if the member is an array, each element is direct-initialized with the corresponding subobject of x;
  • if a member m has rvalue reference type T&&, it is direct-initialized with static_cast<T&&>(x.m);
  • otherwise, the base or member is direct-initialized with the corresponding base or member of x.

Virtual base class subobjects shall be initialized only once by the implicitly-defined copy/move constructor (see [class.base.init]).

Since A has neither bases nor non-static data members, its copy constructor does nothing. B on the other hand has a single non-static data member of type int, so according to the above, its implicit copy constructor direct-initializes mem from the mem member of the object being copied, which is a const lvalue of type int (because the parameter to the copy constructor has type const B&).

The semantics of a direct-initialization of an int from a const lvalue of type int are given by [dcl.init.general]/16.9:

  • [...]
  • Otherwise, the initial value of the object being initialized is the (possibly converted) value of the initializer expression. A standard conversion sequence ([conv]) is used to convert the initializer expression to a prvalue of the cv-unqualified version of the destination type; no user-defined conversions are considered. If the conversion cannot be done, the initialization is ill-formed. When initializing a bit-field with a value that it cannot represent, the resulting value of the bit-field is implementation-defined. [...]

The destination type is int, so we must form a standard conversion sequence from an lvalue of const int to a prvalue of int. The standard conversion that can accomplish this is the lvalue-to-rvalue conversion. See [conv.lval]/1 (footnotes omitted):

A glvalue of a non-function, non-array type T can be converted to a prvalue. If T is an incomplete type, a program that necessitates this conversion is ill-formed. If T is a non-class type, the type of the prvalue is the cv-unqualified version of T. Otherwise, the type of the prvalue is T.

Lvalue-to-rvalue conversions are applied when the rules of the language either explicitly require them to be performed, or when the rules of the language call for a standard conversion sequence to be performed from some source type to some destination type and the lvalue-to-rvalue conversion ends up being a necessary step in that standard conversion sequence. The copy-initialization of an empty class type such as A from the same type is not one of these situations since, based on the above, it is specified to have the behaviour of calling a constructor (not performing a standard conversion) and there is no other rule that demands the lvalue-to-rvalue conversion in such a case. However, copying a glvalue of a scalar type always entails an lvalue-to-rvalue conversion.

Morgen answered 3/3 at 22:20 Comment(2)
Before marking this answer as correct can you tell me why when we change B b; to constexpr B b; the program starts compiling? I mean even then the same logic is applied and the copy ctor does the same thing(copies mem). So lvalue to rvalue happens in that case also but then it compiles there.Riotous
@Riotous An lvalue-to-rvalue conversion is allowed in a constant expression when it's applied to a variable that is "usable in constant expressions". See eel.is/c++draft/expr.const#5.9.1Morgen
W
-1

When you pass a scalar type by-value to a function, then the initialization of the function parameter will directly involve an lvalue-to-rvalue conversion to read the value of the passed object.

When you pass a class type by-value to a function, then you are actually initializing the function parameter with a call to the classes copy constructor.

In the call to the copy constructor the source object is taken by-reference, so there is no immediate lvalue-to-rvalue conversion there.

An lvalue-to-rvalue conversion will happen only if inside the copy constructor a (scalar) non-static data member of the class is copied (which again requires obtaining its value).

Watchdog answered 3/3 at 9:53 Comment(8)
But if we change B b; to constexpr B b; then program starts compiling. Why is that? I mean then also the copy ctor is do memberwise copy.Riotous
@Riotous Because lvalue-to-rvalue conversions are only forbidden in a constant expression if the object on which the conversion is applied neither has its lifetime started during the expression evaluation, nor is usable in constant expressions, which constexpr variables are however.Watchdog
@Riotous From a more practical perspective: If the class is empty it has no state, so there is nothing "unknown" about it at compile-time. If the variable is constexpr, then its value is required to be known at compile-time as well, regardless of its state.Watchdog
Ok I have seen the same explanation in other SO posts also but I want to see exactly where the standard mentions this(like empty class has no state and nothing unknown about it). I want this question to be a canonical for all those questions. So it should cite exact standard references. Because this seems common SO question.Riotous
@Riotous Unfortunately the reasoning the standard is a bit complex. It doesn't consider empty classes specifically. The standard does not make an attempt at explaining this practical reasoning, although it is specified exactly in such a way that it will be the outcome.Watchdog
@Riotous Instead you have to start with timsong-cpp.github.io/cppwp/n4868/expr.const#5.8, and a handful of paragraphs earlier in [expr.const] to define "usable in constant expressions". Then to see whether there is a lvalue-to-rvalue conversion you need multiple iterations through [dcl.init] and [class.copy.ctor] and finally will (only for scalars) end with timsong-cpp.github.io/cppwp/n4868/dcl.init#general-16.9 requiring a standard conversion sequence which per definition in [conv] can include an lvalue-to-rvalue conversion. I don't really feel like writing all of it down.Watchdog
@Watchdog I feel this answer is rushed. You should take your time and add the relevant references and quote them in the answer.Instauration
@Watchdog This answer does not explain the problem in a language-lawyer'ed way. I am already aware of the layman's explanation. I want an authoritative answer.Riotous

© 2022 - 2024 — McMap. All rights reserved.