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.
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]). Letx
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 typeT&&
, it is direct-initialized withstatic_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. IfT
is an incomplete type, a program that necessitates this conversion is ill-formed. IfT
is a non-class type, the type of the prvalue is the cv-unqualified version ofT
. Otherwise, the type of the prvalue isT
.
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.