Why Is a Constexpr Function on a Reference Not Constexpr

Why is a constexpr function on a reference not constexpr?

Unfortunately, the standard states that in a class member access expression The postfix expression before the dot or arrow is evaluated;63 [expr.ref]/1. A postfix expression is a in a.b. The note is really interesting because this is precisely the case here:

63) If the class member access expression is evaluated, the subexpression evaluation happens even if the result is unnecessary to determine the value of the entire postfix expression, for example if the id-expression denotes a static member.

So data is evaluated even if it would not be necessary and the rule fore constant expression applies on it too.

Why does a constexpr function behave differently for a reference?

As is usual with constant expressions, we need to consult the list in [expr.const] and see if any sub-expression we wrote is disallowed. In this case the pertinent bullet is this one:

2.11 an id-expression that refers to a variable or data member of reference type unless the reference has a preceding initialization and
either

  • it is initialized with a constant expression or
  • its lifetime began within the evaluation of e;

That's what knocks ref out of the water. It did not come into existence with the evaluation of count(ref) since it's declared beforehand, and it's not initialized with a constant expression. That may be a 7, but the actual initializer is a temporary object, since that's what the reference binds with.

As for value being usable. That is because there is no bullet that disallows value itself. And the reference argument now has its lifetime begin with the evaluation of count(value), and not beforehand. So it's a valid reference to create in a constant expression, but it just can't be used to read value.

Using a `constexpr` function on a variable passed as lvalue in a non-constexpr function

Below, I answer as to why your code doesn't work. Focusing on your use-case: as the others have said, std::array::size is not static, and all std::size does is call that non-static function. Your best bet is to simply add a static size function to your Vector class:

static constexpr auto size() {
return D;
}

Your implementation of cross will not work because you cannot use non-constant expressions to initialize templates. See this SO answer on why function arguments are not constant expressions.

Basically, calling your cross function requires generating a new instance of the cross_dispatch structure for every different value of std::size(vec1), which also requires knowing the address of every given vec1 at compile-time since std::size calls a non-static function. You should be able to see from this that the compiler simply can't know which instances of cross_dispatch need to be created.

Above, I provided a solution specific to your use-case. If you were doing more than measuring the size of your Vector, a second solution would involve passing the objects as template parameters instead (which will require them to be static):

template <typename VectorType, VectorType const& vec1, VectorType const& vec2>
constexpr VectorType cross()
{
return cross_dispatch<std::size(vec1), VectorType>::apply(vec1, vec2);
}

int main()
{
static vector p1 {1.,2.,3.};
static vector q1 {1.,2.,3.};

cross<vector, p1, q1>();
}

Because p1 and q1 are static, their addresses can be known at compile-time, allowing cross's template to be initialized. Template parameters do not change at run-time, so std::size(vec1) is now a constant expression.

Function is not usable as a 'constexpr' function

In order to evaluate this:

  if constexpr (p(x)) {

We need for p(x) to be a constant expression. The rules for whether something qualifies as a constant expression or not are based on a list of things that you're not allowed to do.

When x is a basic_type<int> and p is a function that takes a basic_type<int> by value, there are simply no rules that we are violating. This is an empty type, so copying it (as we're doing here) doesn't actually involve any kind of read. This just works.


But when x is an int and p is a function that takes an int by value, this also requires copying x but this time it involves reading the value of x. Because, of course, gotta initialize the parameter somehow. And this does run afoul of a rule: [expr.const]/8 says we're not allowed to perform:

an lvalue-to-rvalue conversion unless it is applied to

  • a non-volatile glvalue that refers to an object that is usable in constant expressions, or
  • a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of E;

An lvalue-to-rvalue conversion is what happens when we read the value of a variable, and neither of those bullets apply. It doesn't matter here that you don't actually care what the value is, since p doesn't use it. In order to be able to even invoke p, you have to copy x, and you're not allowed to do that. Hence the error.


However, your lambda here doesn't actually need the value, just the type, so you could instead write this:

    return foo(
[]<typename T>(T const&)
{
return std::is_integral_v<std::decay_t<T>>;
},
0);

Now we're no longer copying x into the lambda, since the lambda no longer takes by value - it takes by reference. As a result, we're not violating the lvalue-to-rvalue conversion rule (or any other rule) and this is now a valid constant expression.


Then, as a bonus, if you change foo to take x by reference (because, again, you don't actually care about the value, so why not):

consteval auto foo(auto p, auto const& x) noexcept {
if constexpr (p(x)) {
return 1;
} else {
return 0;
}
}

Then both variants become ill-formed. Both the basic_type<int> and int versions (regardless of whether you take the int by value or by reference). For more on this case, see the constexpr array size problem which I'm currently trying to resolve with P2280.

A constexpr function is not required to return a constant expression?

A (non-template) constexpr function must have at least one execution path that returns a constant expression; formally, there must exist argument values such that "an invocation of the function [...] could be an evaluated subexpression of a core constant expression" ([dcl.constexpr]/5). For example (ibid.):

constexpr int f(bool b) { return b ? throw 0 : 0; }     // OK
constexpr int f() { return f(true); } // ill-formed, no diagnostic required

Here int f(bool) is allowed to be constexpr because its invocation with argument value false returns a constant expression.

It is possible to have a constexpr function that cannot ever return a constant expression if it is a specialization of a function template that could have at least one specialization that does return a constant expression. Again, with the above:

template<bool B> constexpr int g() { return f(B); }    // OK
constexpr int h() { return g<true>(); } // ill-formed, no diagnostic required

how to initialize a constexpr reference

  1. Are constexpr references ever useful? (i.e., "better" than const references)

They are guaranteed to be initiailized before the program starts, whereas a reference to const can be initialized during dynamic initialization, after the program starts running.


  1. If yes, how can I effectively define them?

A constexpr reference has to bind to a global, not a local variable (or more formally, it has to bind to something with static storage duration).

A reference is conceptually equivalent to taking the address of the variable, and the address of a local variable is not a constant (even in main which can only be called once and so its local variables are only initialized once).

Why initializing a 'const reference to base' object from 'constexpr derived' object doesn't yields constant expression?

The explanation given by Clang is correct.

Constexpr doesn't change the storage of a variable (where it's put in memory). If you declare a non-static constexpr variable, it'll still go onto the stack, just like all other non-static local variables do.

int test() {
constexpr int a = 3; //a gets created here and put on the stack
const int *b = &a; //Pointer to a location on the stack
} //a gets destroyed when the function ends

This means that the address of that local variable can change with every invocation of the function. There can also be multiple copies of the variable at any given time, i.e. if multiple threads execute the function or if the function recurses.

The overall net effect is that the address of a local variable can never be a constant expression, no matter if that variable is const, constexpr, volatile, or anything else.

The static keyword changes this. It "promotes" the local variable into static storage, meaning that it behaves like a global variable that is only visible in that function. In particular, the variable has a fixed address and there is only one copy of the variable, no matter how often (if at all) the function gets executed. The address of a global variable is a constant expression.

If you want a single, global, constant instance of derived d, just add static in front of it like Clang suggests.

Constexpr Class taking const references not compiling

Yeah, this rule is one of the more complex ones as far as constant evaluation is concerned.

Basically, you cannot have a constexpr reference to an object that doesn't have static storage duration. Taking a reference to an object is basically copying its address - and in order for an object's address to be a constant expression, the address itself needs to be constant - so it has to persist. That is, it needs to be static.

So if you change the things you're referring to to have static storage duration instead, everything works:

static constexpr int a = 3;
static constexpr int b = 4;

constexpr Operation op(a, b); // now ok

The specific rule your program violates is [expr.const]/10, and T.C. helped me understand how it applies. Declaring a constexpr variable requires the initialization to be a constant expression ([dcl.constexpr/10]):

In any constexpr variable declaration, the full-expression of the initialization shall be a constant expression.

We don't say this, but it makes sense and certainly helps resolve this particular situation, but the "full-expression of the initialization" can be interpreted as a prvalue -- since a prvalue is an expression whose evaluation initializes an object ([basic.lval]/1).

Now, [expr.const]/10 reads:

A constant expression is either a glvalue core constant expression [...], or or a prvalue core constant expression whose value satisfies the following constraints:

  • if the value is an object of class type, each non-static data member of reference type refers to an entity that is a permitted result of a constant expression,
  • [...],
  • if the value is an object of class or array type, each subobject satisfies these constraints for the value.

An entity is a permitted result of a constant expression if it is an object with static storage duration that either is not a temporary object or is a temporary object whose value satisfies the above constraints, or if it is a non-immediate function.

The initialization Operation(a, b) is a prvalue, so we need each reference data member to refer to an entity that is permitted as a result of a constant expression. Our reference data members refer to a and b, neither of which has static storage duration nor are temporaries nor are non-immediate functions. Hence, the overall initialization isn't a constant expression, and is ill-formed.

Making a and b static gives them static storage duration, which makes them permitted results of constant expressions, which makes the prvalue initialization satisfy all the requirements, which makes the declaration of op valid.


This is all a long winded way of saying: when dealing with constant evaluation, everything everywhere has to be constant all the way down. Some of our ways of wording this are very complex (like this one), but it's based on the fundamental idea that the model of constant evaluation is basically like pausing evaluating the code to go run a separate program to produce an answer. Producing op requires these addresses to be known, fixed things - and that only happens for static storage duration.

constexpr function with unused reference argument – gcc vs clang

GCC is correct here.

According to [expr.const]/4:

An expression e is a core constant expression unless the evaluation
of e, following the rules of the abstract machine, would evaluate
one of the following expressions:

  • ...
  • in a lambda-expression, a reference to [...] a variable with automatic storage duration defined outside that lambda-expression,
    where the reference would be an odr-use; ...
  • ...

k(i) odr-uses i thus k(i) is not a constant expression in the lambda expression, so this code is ill-formed.



Related Topics



Leave a reply



Submit