Copy/assignment of fundamental types - c++

What does the standard say about copy/assignment of fundamental types?
For class types, we have copy constructor, assignment operator, which takes the right hand side as a reference (it must be a reference, otherwise we had infinite recursion):
struct Foo {
Foo(const Foo &);
};
How does this defined for fundamental types?
Look at this example:
const Foo foo;
Foo f = foo;
const int a = 2;
int b = a;
Here, f = foo; odr-uses foo, as copy-constructor takes a reference, right?. If copy of fundamental types had a reference parameter, then b = a would odr-use a as well. Is it the case? If not, how is it handled?

We can trace it. Starting at [dcl.init].
(17.8) - Otherwise, the initial value of the object being initialized
is the (possibly converted) value of the initializer expression.
Standard conversions will be used, if necessary, to convert the
initializer expression to 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 standard conversion in this case would be the lvalue-to-rvalue conversion on a. But that doesn't odr-use a. For we see in [basic.def.odr]
2 A variable x whose name appears as a potentially-evaluated expression
ex is odr-used by ex unless applying the lvalue-to-rvalue conversion
to x yields a constant expression that does not invoke any non-trivial
functions and, if x is an object, ex is an element of the set of
potential results of an expression e, where either the
lvalue-to-rvalue conversion is applied to e, or e is a discarded-value
expression.
a is a constant expression and substitution of a for x and ex above demonstrates it holds the other half of the condition, so it's not odr-used.

Related

Does lvalue-to-rvalue conversion is applied to non-type lvalue template argument?

struct S{
constexpr S() {};
};
template <auto x> void f();
int main() {
S s{};
f<s>();
}
First off, per [temp.arg.nontype]/1
If the type T of a template-parameter (13.2) contains a placeholder
type (9.2.9.6) or a placeholder for a deduced class type (9.2.9.7),
the type of the parameter is the type deduced for the variable x in
the invented declaration
T x = template-argument;
If a deduced parameter type is not permitted for a template-parameter
declaration (13.2), the program is ill-formed.
Our template parameter contains a placeholder type auto, so the type of the parameter is the type deduced for the variable x in the invented declaration auto x = s; In this case, the type of the parameter is S, and S is a permitted type for the parameter declaration because S is a structural literal type.
Second, per [temp.arg.nontype]/2
A template-argument for a non-type template-parameter shall be a
converted constant expression (7.7) of the type of the
template-parameter.
This means the template-argument s shall be a converted constant expression. So per [expr.const]/10:
A converted constant expression of type T is an expression, implicitly
converted to type T, where the converted expression is a constant
expression and the implicit conversion sequence contains only
[..]
(10.2) — lvalue-to-rvalue conversions (7.3.2)
[..]
I'm not sure whether or not the lvalue s is converted to prvalue before any implicit conversions are applied to it. Note the definition of converted constant expression in C++14 is relatively changed. N4140 §5.19 [expr.const]/3: (emphasis mine)
A converted constant expression of type T is an expression, implicitly
converted to a prvalue of type T, where the converted expression is a
core constant expression and the implicit conversion sequence contains
only [..]
So per C++14, It's guaranteed that the converted expression is converted to a prvalue before any implicit conversion is applied to it.
I'm not so sure whether or not an lvalue-to-rvalue conversion is applied to template-argument s. In other words, I'm not sure that the lvalue s is converted to a prvalue.
So in general, if I passed an lvalue as a template argument for a non-type template parameter, does an lvalue-to-rvalue conversion applied to that lvalue?
And aside from that, Is the object s constant-initialized?
The meaning of [expr.const]/10 is that whenever an expression appears in a context that requires a "converted constant expression of type T", that expression is "implicitly converted to type T" and the additional restrictions in [expr.const]/10 shall apply.
So s is not "converted to a prvalue before any implicit conversions are applied to it". Rather, s is implicitly converted to type S, and this implicit conversion might involve some standard and/or user-defined conversions, such as an lvalue-to-rvalue conversion. To know whether or not it does, we would have to look at the rules for implicit conversions, which are in [conv.general]. In particular [conv.general]/6 states that
The effect of any implicit conversion is the same as performing the corresponding declaration and initialization and then using the temporary variable as the result of the conversion. The result is an lvalue if T is an lvalue reference type or an rvalue reference to function type ([dcl.ref]), an xvalue if T is an rvalue reference to object type, and a prvalue otherwise.
The expression E is used as a glvalue if and only if the initialization uses it as a glvalue.
I believe the wording here is a relic of older standard editions. What it should say (in modern language) is that when T is a non-reference type, an implicit conversion of E to T yields a prvalue that initializes its result object (call it t) as if by T t = E;.
So we consider S t = s; What does this initialization do? It calls the copy constructor of S to initialize t; [dcl.init.general]/16.6.2.1. There is no lvalue-to-rvalue conversion in this scenario. (Lvalue-to-rvalue conversions on class types are rare, but do occur in some places in the language, e.g., [expr.call]/12, [expr.cond]/7. If an lvalue-to-rvalue conversion were performed on s, it would also call the copy constructor; [conv.lval]/3.2. But in this particular case, the rules of the language do not require an lvalue-to-rvalue conversion.)
Consequently, the result of using s as a template argument for a template parameter of type S is that a prvalue is generated that initializes its result object by calling the copy constructor with s as the argument. (This particular copy constructor is implicitly defined to not do anything, since there are no subobjects to copy.)
This answers your question regarding whether an lvalue-to-rvalue conversion is applied. (You might be wondering what actually happens to the prvalue and whether the copy constructor actually gets called, but that's a separate topic that I don't want to get into right now, because it would make this answer too long.)
As for whether s is constant-initialized, yes it is. It satisfies (2.1) (since it has an initializer) and (2.2) since the full-expression of its initialization is a constant expression (there is nothing to stop it from being a constant expression, since it does nothing other than calling the constexpr default constructor, which itself does nothing). I'm not sure how this is relevant to your other question, though.

Default constructor expression and lvalues

My C++ colleagues and I ran into a curious construct:
struct A { int i; };
void foo(A const& a);
int main() {
foo(A() = A{2}); // Legal
}
The A() = A{2} expression completely befuddled us as it appears to be assigning A{2} to a temporary, default-constructed object. But see it in compiler explorer (https://gcc.godbolt.org/z/2LsfSk). It appears to be a legal statement (supported by GCC 9 and Clang 9), as are the following statements:
struct A { int i; };
int main() {
A() = A{2};
auto a = A() = A{3};
}
So it appears, then, that in some contexts A() is an lvalue. Or is something else going on here? Would appreciate some explanation and, preferably, a reference to the C++17 standard.
Update: #Brian found that this is a duplicate of assigning to rvalue: why does this compile?. But would really appreciate if someone could find the appropriate reference in the C++ standard.
A{} is always an rvalue per [expr.type.conv]
1 A simple-type-specifier or typename-specifier followed by a parenthesized optional expression-list or by a braced-init-list (the initializer) constructs a value of the specified type given the initializer.
If the type is a placeholder for a deduced class type, it is replaced by the return type of the function selected by overload resolution for class template deduction for the remainder of this subclause.
2 If the initializer is a parenthesized single expression, the type conversion expression is equivalent to the corresponding cast expression.
Otherwise, if the type is cv void and the initializer is () or {} (after pack expansion, if any), the expression is a prvalue of the specified type that performs no initialization.
Otherwise, the expression is a prvalue of the specified type whose result object is direct-initialized with the initializer.
If the initializer is a parenthesized optional expression-list, the specified type shall not be an array type.
emphasis mine
The reason these works is here is nothing in the standard to stop it from working.
For built in types like int there is [expr.ass]/1
The assignment operator (=) and the compound assignment operators all group right-to-left. All require a modifiable lvalue as their left operand; their result is an lvalue referring to the left operand.
So this stops you from doing int{} = 42;. This section doesn't apply to classes, though. If we look in [class.copy.assign] there is nothing that says that an lvalue is required, but the first paragraph does state
A user-declared copy assignment operator X​::​operator= is a non-static non-template member function of class X with exactly one parameter of type X, X&, const X&, volatile X&, or const volatile X&
Which means
A{} = A{2};
is actually
A{}.operator=(A{2})
Which is legal to do on an rvalue class object since the default operator = for your class has no ref-qualifier to stop it from being called on rvalues. If you add
A& operator=(const A& a) & { i = a.i; }
to A instead of using the default assignment operator then
A{} = A{2};
would no longer compile since the operator= will only work on lvalues now.

Static Constexpr in Struct not found [duplicate]

Given the following code:
struct A { static constexpr int a[3] = {1,2,3}; };
int main () {
int a = A::a[0];
int b [A::a[1]];
}
is A::a necessarily odr-used in int a = A::a[0]?
Note: This question represents a less flamey/illogical/endless version of a debate in the Lounge.
First use of A::a:
int a = A::a[0];
The initializer is a constant expression, but that doesn't stop A::a from being odr-used here. And, indeed, A::a is odr-used by this expression.
Starting from the expression A::a[0], let's walk through [basic.def.odr](3.2)/3 (for future readers, I'm using the wording from N3936):
A variable x [in our case, A::a] whose name appears as a potentially-evaluated expression ex [in our case, the id-expression A::a] is odr-used unless
applying the lvalue-to-rvalue conversion to x yields a constant expression [it does]
that does not invoke any non-trivial functions [it does not] and,
if x is an object [it is],
ex is an element of the set of potential results of an expression e, where either the lvalue-to-rvalue conversion is applied to e, or e is a discarded-value expression.
So: what possible values of e are there? The set of potential results of an expression is a set of subexpressions of the expression (you can check this by reading through [basic.def.odr](3.2)/2), so we only need to consider expressions of which ex is a subexpression. Those are:
A::a
A::a[0]
Of these, the lvalue-to-rvalue conversion is not applied immediately to A::a, so we only consider A::a[0]. Per [basic.def.odr](3.2)/2, the set of potential results of A::a[0] is empty, so A::a is odr-used by this expression.
Now, you could argue that we first rewrite A::a[0] to *(A::a + 0). But that changes nothing: the possible values of e are then
A::a
A::a + 0
(A::a + 0)
*(A::a + 0)
Of these, only the fourth has an lvalue-to-rvalue conversion applied to it, and again, [basic.def.odr](3.2)/2 says that the set of potential results of *(A::a + 0) is empty. In particular, note that array-to-pointer decay is not an lvalue-to-rvalue conversion ([conv.lval](4.1)), even though it converts an array lvalue to a pointer rvalue -- it's an array-to-pointer conversion ([conv.array](4.2)).
Second use of A::a:
int b [A::a[1]];
This is no different from the first case, according to the standard. Again, A::a[1] is a constant expression, thus this is a valid array bound, but a compiler is still permitted to emit code at runtime to compute this value, and the array bound still odr-uses A::a.
Note in particular that constant expressions are (by default) potentially-evaluated expressions. Per [basic.def.odr](3.2)/2:
An expression is potentially evaluated unless it is an unevaluated operand (Clause 5) or a subexpression thereof.
[expr](5)/8 just redirects us to other subclauses:
In some contexts, unevaluated operands appear (5.2.8, 5.3.3, 5.3.7, 7.1.6.2). An unevaluated operand is not evaluated.
These subclauses say that (respectively) the operand of some typeid expressions, the operand of sizeof, the operand of noexcept, and the operand of decltype are unevaluated operands. There are no other kinds of unevaluated operand.
Yes, A::a is odr-used.
In C++11, the relevant wording is 3.2p2 [basic.def.odr]:
[...] A variable whose name appears as a potentially-evaluated expression is odr-used unless it is an object that satisfies the requirements for appearing in a constant expression (5.19) and the lvalue-to-rvalue conversion (4.1) is immediately applied. [...]
The name of the variable A::a appears in the declaration int a = A::a[0], in the full-expression A::a[0], which is a potentially-evaluated expression. A::a is:
an object
that satisfies the requirements for appearing in a constant expression
However, the lvalue-to-rvalue conversion is not immediately applied to A::a; it is applied to the expression A::a[0]. Indeed, lvalue-to-rvalue conversion may not apply to an object of array type (4.1p1).
So A::a is odr-used.
Since C++11, the rules have been broadened somewhat. DR712 Are integer constant operands of a conditional-expression "used?" introduces the concept of the set of potential results of an expression, which allows expressions such as x ? S::a : S::b to avoid odr-use. However, while the set of potential results respects such operators as the conditional operator and comma operator, it does not respect indexing or indirection; so A::a is still odr-used in the current drafts for C++14 (n3936 as of date).
[I believe this is a condensed equivalent to Richard Smith's answer, which however does not mention the change since C++11.]
At When is a variable odr-used in C++14? we discuss this issue and possible wording changes to section 3.2 to allow indexing or indirecting an array to avoid odr-use.
No, it is not odr-used.
First, both your array and its elements are of literal type:
[C++11: 3.9/10]: A type is a literal type if it is:
a scalar type; or
a class type (Clause 9) with
a trivial copy constructor,
no non-trivial move constructor,
a trivial destructor,
a trivial default constructor or at least one constexpr constructor other than the copy or move constructor, and
all non-static data members and base classes of literal types; or
an array of literal type.
Now we look up the odr-used rules:
[C++11: 3.2/2]: [..] A variable or non-overloaded function whose name appears as a potentially-evaluated expression is odr-used unless it is an object that satisfies the requirements for appearing in a constant expression (5.19) and the lvalue-to-rvalue conversion (4.1) is immediately applied. [..]
And here we've been referred to the rules on constant expressions, which contain nothing prohibiting your initialiser from being a constant expression; the pertinent passages are:
[C++11: 5.19/2]: A conditional-expression is a constant expression unless it involves one of the following as a potentially evaluated subexpression [..]:
[..]
an lvalue-to-rvalue conversion (4.1) unless it is applied to
a glvalue of integral or enumeration type that refers to a non-volatile const object with a preceding initialization, initialized with a constant expression, or
a glvalue of literal type that refers to a non-volatile object defined with constexpr, or that refers to a sub-object of such an object, or
a glvalue of literal type that refers to a non-volatile temporary object initialized with a constant expression;
[..]
(Don't be put off by the name of the production, "conditional-expression": it is the only production of constant-expression and is thus the one we're looking for.)
Then, thinking about the equivalence of A::a[0] to *(A::a + 0), after the array-to-pointer conversion you have an rvalue:
[C++11: 4.2/1]: An lvalue or rvalue of type "array of N T" or "array of unknown bound of T" can be converted to a prvalue of type "pointer to T". The result is a pointer to the first element of the array.
Your pointer arithmetic is then performed on this rvalue and the result is also an rvalue, used to initialise a. No lvalue-to-rvalue conversion here whatsoever, so still nothing violating "the requirements for appearing in a constant expression".

When is a variable odr-used in C++14?

The C++14 draft (N3936) states in §3.2/3:
A variable x whose name appears as a potentially-evaluated expression ex is odr-used unless applying the lvalue-to-rvalue conversion (4.1) to x yields a constant expression (5.19) that does not invoke any non-trivial functions and, if x is an object, ex is an element of the set of potential results of an expression e, where either the lvalue-to-rvalue conversion (4.1) is applied to e, or e is a discarded-value expression (Clause 5).
This doesn't make any sense to me: If an expression e is a discarded-value expression depends on the context, in which e is used. Every expression used in an expression-statement (§6.2) is a discarded-value expression. If the lvalue-to-rvalue conversion is applied to e also depends on the context e is used in.
Moreover, what does it mean for an expression to be in the set of potential results of another expression. One needs a notion of equality of expressions to be able to determine membership of a set. But we don't have referential transparency, so I cannot see how this could be achieved.
Why was this changed from C++11 to C++14? And how should this be interpreted? As it stands, it doesn't make sense.
The purpose of odr-use
Informally, odr-use of a variable means the following:
If any expression anywhere in the program takes the address of or binds a reference directly to an object, this object must be defined.
Clarification in the latest draft
In the latest version of the spec §3.2 has been clarified (see Draft C++14 on GitHub):
2 An expression is potentially evaluated unless it is an unevaluated operand (Clause 5) or a subexpression thereof. The set of potential results of an expression e is defined as follows:
If e is an id-expression (5.1.1), the set contains only e.
If e is a class member access expression (5.2.5), the set contains the potential results of the object expression.
If e is a pointer-to-member expression (5.5) whose second operand is a constant expression, the set contains the potential results of the object expression.
If e has the form (e1), the set contains the potential results of e1.
If e is a glvalue conditional expression (5.16), the set is the union of the sets of potential results of the second and third operands.
If e is a comma expression (5.18), the set contains the potential results of the right operand.
Otherwise, the set is empty.
[ Note: This set is a (possibly-empty) set of id-expressions, each of which is either e or a subexpression of e.
[ Example: In the following example, the set of potential results of the initializer of n contains the first S::x subexpression, but not the second S::x subexpression.
struct S { static const int x = 0; };
const int &f(const int &r);
int n = b ? (1, S::x) // S::x is not odr-used here
: f(S::x); // S::x is odr-used here, so
// a definition is required
—end example ] —end note ]
3 A variable x whose name appears as a potentially-evaluated expression ex is odr-used by ex unless applying the lvalue-to-rvalue conversion (4.1) to x yields a constant expression (5.19) that does not invoke any nontrivial functions and, if x is an object, ex is an element of the set of potential results of an expression e, where either the lvalue-to-rvalue conversion (4.1) is applied to e, or e is a discarded-value expression (Clause 5).
What was the situation in C++11?
§3.2/2 in C++11 reads:
An expression is potentially evaluated unless it is an unevaluated operand (Clause 5) or a subexpression thereof. A variable whose name appears as a potentially-evaluated expression is odr-used unless it is an object that satisfies the requirements for appearing in a constant expression (5.19) and the lvalue-to-rvalue conversion (4.1) is immediately applied.
The problem with these wordings was DR 712. Consider this example:
struct S {
static const int a = 1;
static const int b = 2;
};
int f(bool x) {
return x ? S::a : S::b;
}
Since S::a and S::b are lvalues the conditional expression x ? S::a : S::b is also an lvalue. This means that the lvalue-to-rvalue conversion is not immediately applied to S::a and S::b, but to the result of the conditional expression. This means that by the wording of C++11, these static data members are odr-used and a definition is required. But actually only the values are used, hence it is not neccessary to define the static data members - a declaration would suffices. The new wording of draft C++14 solves this.
Does the new wording resolve all issues?
No. In the following example the variable S::a is still odr-used:
struct S { static constexpr int a[2] = {0, 1}; };
void f() {
auto x = S::a[0];
}
Hence I submitted a new issue to add the following bullet to §3.2/2:
if e is a glvalue subscripting expression (5.2.1) of the form E1[E2], the set contains the potential results of E1.

Is a constexpr array necessarily odr-used when subscripted?

Given the following code:
struct A { static constexpr int a[3] = {1,2,3}; };
int main () {
int a = A::a[0];
int b [A::a[1]];
}
is A::a necessarily odr-used in int a = A::a[0]?
Note: This question represents a less flamey/illogical/endless version of a debate in the Lounge.
First use of A::a:
int a = A::a[0];
The initializer is a constant expression, but that doesn't stop A::a from being odr-used here. And, indeed, A::a is odr-used by this expression.
Starting from the expression A::a[0], let's walk through [basic.def.odr](3.2)/3 (for future readers, I'm using the wording from N3936):
A variable x [in our case, A::a] whose name appears as a potentially-evaluated expression ex [in our case, the id-expression A::a] is odr-used unless
applying the lvalue-to-rvalue conversion to x yields a constant expression [it does]
that does not invoke any non-trivial functions [it does not] and,
if x is an object [it is],
ex is an element of the set of potential results of an expression e, where either the lvalue-to-rvalue conversion is applied to e, or e is a discarded-value expression.
So: what possible values of e are there? The set of potential results of an expression is a set of subexpressions of the expression (you can check this by reading through [basic.def.odr](3.2)/2), so we only need to consider expressions of which ex is a subexpression. Those are:
A::a
A::a[0]
Of these, the lvalue-to-rvalue conversion is not applied immediately to A::a, so we only consider A::a[0]. Per [basic.def.odr](3.2)/2, the set of potential results of A::a[0] is empty, so A::a is odr-used by this expression.
Now, you could argue that we first rewrite A::a[0] to *(A::a + 0). But that changes nothing: the possible values of e are then
A::a
A::a + 0
(A::a + 0)
*(A::a + 0)
Of these, only the fourth has an lvalue-to-rvalue conversion applied to it, and again, [basic.def.odr](3.2)/2 says that the set of potential results of *(A::a + 0) is empty. In particular, note that array-to-pointer decay is not an lvalue-to-rvalue conversion ([conv.lval](4.1)), even though it converts an array lvalue to a pointer rvalue -- it's an array-to-pointer conversion ([conv.array](4.2)).
Second use of A::a:
int b [A::a[1]];
This is no different from the first case, according to the standard. Again, A::a[1] is a constant expression, thus this is a valid array bound, but a compiler is still permitted to emit code at runtime to compute this value, and the array bound still odr-uses A::a.
Note in particular that constant expressions are (by default) potentially-evaluated expressions. Per [basic.def.odr](3.2)/2:
An expression is potentially evaluated unless it is an unevaluated operand (Clause 5) or a subexpression thereof.
[expr](5)/8 just redirects us to other subclauses:
In some contexts, unevaluated operands appear (5.2.8, 5.3.3, 5.3.7, 7.1.6.2). An unevaluated operand is not evaluated.
These subclauses say that (respectively) the operand of some typeid expressions, the operand of sizeof, the operand of noexcept, and the operand of decltype are unevaluated operands. There are no other kinds of unevaluated operand.
Yes, A::a is odr-used.
In C++11, the relevant wording is 3.2p2 [basic.def.odr]:
[...] A variable whose name appears as a potentially-evaluated expression is odr-used unless it is an object that satisfies the requirements for appearing in a constant expression (5.19) and the lvalue-to-rvalue conversion (4.1) is immediately applied. [...]
The name of the variable A::a appears in the declaration int a = A::a[0], in the full-expression A::a[0], which is a potentially-evaluated expression. A::a is:
an object
that satisfies the requirements for appearing in a constant expression
However, the lvalue-to-rvalue conversion is not immediately applied to A::a; it is applied to the expression A::a[0]. Indeed, lvalue-to-rvalue conversion may not apply to an object of array type (4.1p1).
So A::a is odr-used.
Since C++11, the rules have been broadened somewhat. DR712 Are integer constant operands of a conditional-expression "used?" introduces the concept of the set of potential results of an expression, which allows expressions such as x ? S::a : S::b to avoid odr-use. However, while the set of potential results respects such operators as the conditional operator and comma operator, it does not respect indexing or indirection; so A::a is still odr-used in the current drafts for C++14 (n3936 as of date).
[I believe this is a condensed equivalent to Richard Smith's answer, which however does not mention the change since C++11.]
At When is a variable odr-used in C++14? we discuss this issue and possible wording changes to section 3.2 to allow indexing or indirecting an array to avoid odr-use.
No, it is not odr-used.
First, both your array and its elements are of literal type:
[C++11: 3.9/10]: A type is a literal type if it is:
a scalar type; or
a class type (Clause 9) with
a trivial copy constructor,
no non-trivial move constructor,
a trivial destructor,
a trivial default constructor or at least one constexpr constructor other than the copy or move constructor, and
all non-static data members and base classes of literal types; or
an array of literal type.
Now we look up the odr-used rules:
[C++11: 3.2/2]: [..] A variable or non-overloaded function whose name appears as a potentially-evaluated expression is odr-used unless it is an object that satisfies the requirements for appearing in a constant expression (5.19) and the lvalue-to-rvalue conversion (4.1) is immediately applied. [..]
And here we've been referred to the rules on constant expressions, which contain nothing prohibiting your initialiser from being a constant expression; the pertinent passages are:
[C++11: 5.19/2]: A conditional-expression is a constant expression unless it involves one of the following as a potentially evaluated subexpression [..]:
[..]
an lvalue-to-rvalue conversion (4.1) unless it is applied to
a glvalue of integral or enumeration type that refers to a non-volatile const object with a preceding initialization, initialized with a constant expression, or
a glvalue of literal type that refers to a non-volatile object defined with constexpr, or that refers to a sub-object of such an object, or
a glvalue of literal type that refers to a non-volatile temporary object initialized with a constant expression;
[..]
(Don't be put off by the name of the production, "conditional-expression": it is the only production of constant-expression and is thus the one we're looking for.)
Then, thinking about the equivalence of A::a[0] to *(A::a + 0), after the array-to-pointer conversion you have an rvalue:
[C++11: 4.2/1]: An lvalue or rvalue of type "array of N T" or "array of unknown bound of T" can be converted to a prvalue of type "pointer to T". The result is a pointer to the first element of the array.
Your pointer arithmetic is then performed on this rvalue and the result is also an rvalue, used to initialise a. No lvalue-to-rvalue conversion here whatsoever, so still nothing violating "the requirements for appearing in a constant expression".