Why is a reference parameter in a constexpr function not a constant expression?
Asked Answered
T

5

14

Consider the following function:

template <size_t S1, size_t S2>
auto concatenate(const std::array<uint8_t, S1> &data1,
                 const std::array<uint8_t, S2> &data2)
{
    std::array<uint8_t, data1.size() + data2.size()> result; // <<< error here

    auto iter = std::copy(data1.begin(), data1.end(), result.begin());
    std::copy(data2.begin(), data2.end(), iter);

    return result;
}

int main()
{
    std::array<uint8_t, 1> data1{ 0x00 };
    std::array<uint8_t, 1> data2{ 0xFF };

    auto result = concatenate(data1, data2);
    return 0;
}

When compiled using clang 6.0, using -std=c++17, this function does not compile, because the size member function on the array is not constexpr due to it being a reference. The error message is this:

error: non-type template argument is not a constant expression

When the parameters are not references, the code works as expected.

I wonder why this would be, as the size() actually returns a template parameter, it could hardly be any more const. Whether the parameter is or is not a reference shouldn't make a difference.

I know I could of course use the S1 and S2 template parameters, the function is merely a short illustration of the problem.

Is there anything in the standard? I was very surprised to get a compile error out of this.

Thursby answered 10/1, 2019 at 8:52 Comment(1)
Compiles as given on GCC 8.2. godbolt.org/z/G6_z1v. Please mention compiler and cpp version.Posturize
S
-2

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.

Seedy answered 10/1, 2019 at 12:15 Comment(6)
What's so unfortunate about a.b evaluating a (or f().b evaluating f())?Elderly
@Elderly When b is a static member, a will never be accessed!Seedy
@Elderly But actualy I have just checked, there is a second design error in the definition of array! size is a non static member! That is crazy! There have been too many modification to the language. The entire design of the STL is obsolete.Seedy
@Elderly 1) Why did the standard added "The postfix expression before the dot or arrow is evaluated"?? The fact a member is static or non-static is known at compile time in every translation unit where it is used. 2) Is there a paragraph in the library part of the standard that gives freedom to implementation to choose whether a member is implemented as a static member or non static one? I can not find any good reason to force this.Seedy
What if for some reason you want to apply & operator to size() in generic code? If it is static, &size will be a function pointer, if it is non-static, it will be a member function pointer. For example, such generic code will not work with both "static-augmented" std::array and std::vector uniformly. Making size() a static member function may break some existing code.Piperine
@Evg: That isn't (and wasn't, and won't be) permitted anyway. eel.is/c++draft/constraints#namespace.std-6 You can use a lambda to bridge the gap between std::array::size() and a function pointer, but you can't apply "operator &"Jacquelynejacquelynn
A
12

Because you have evaluated a reference. From [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:

  • ...
  • 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 usable in constant expressions or
    • its lifetime began within the evaluation of e;
  • ...

Your reference parameter has no preceding initialization, so it cannot be used in a constant expression.

You can simply use S1 + S2 instead here.

Athalia answered 10/1, 2019 at 9:15 Comment(0)
P
7

There has been a bug reported on this issue for clang titled: Clang does not allow to use constexpr type conversion in non-type template argument.

The discussion in it points that this is not really a bug.

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:

  • [...]
  • 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;
  • [...]

The above quote is from [expr.const]/2.11 of draft n4659 with emphasis added.

Posturize answered 10/1, 2019 at 9:15 Comment(0)
P
4

This is my favorite weird constexpr issue: the constexpr array size problem, which was resolved by P2280.

Other answers have already pointed out that data.size() didn't used to be valid for use in a constant expression simply because data is a reference that is unknown to the constant evaluator. That's now changed - you don't actually need to read through data in any way to find the answer, so there's nothing anymore that should prevent that from working.

Until then, since you are already deducing the extents for data1 and data2, you can just use S1 and S2 instead of data.size() and data2.size(), respectively. Or any number of other workarounds.

As of this writing (Jan 2024), gcc trunk (which would be the 14.0 release) now compiles the original example. Clang and MSVC do not yet implement P2280.


This is a blog post written by me on my blog.

This is a proposal written by me for WG21.

Patsy answered 10/1 at 18:32 Comment(2)
Consider <sup><sup>&dagger;</sup></sup><sup>This is a blog post written by me on my blog.</sup><sup><sup>&Dagger;</sup>This is a proposal written by me for WG21.</sup>Brunobruns
@Brunobruns Huh?Patsy
M
3

Your code is valid, you just need a recent compiler (e.g. GCC 14) to make it work (https://godbolt.org/z/qo3ovEMTY). The wording in [expr.const] p8 is:

During the evaluation of an expression E as a core constant expression, all id-expressions and uses of *this that refer to an object or reference whose lifetime did not begin with the evaluation of E are treated as referring to a specific instance of that object or reference whose lifetime and that of all subobjects (including all union members) includes the entire constant evaluation. [...]

Essentially, an id-expression array1 would refer to an unspecified object which isn't usable in a constant expression, but the expression array1 in itself is allowed, as well as any use of array1 that doesn't access the object.

The standard contains this example:

template <typename T, size_t N>
constexpr size_t array_size(T (&)[N]) {
  return N;
}

void use_array(int const (&gold_medal_mel)[2]) {
  constexpr auto gold = array_size(gold_medal_mel);     // OK
}
// [...]
struct Swim {
    constexpr int phelps() { return 28; }
    // [...]
};

void splash(Swim& swam) {
    static_assert(swam.phelps() == 28);                 // OK
    // [...]
}

This (and your code) would have been ill-formed prior to C++23. In particular, the lines marked OK used to be ill-formed. The example with .phelps() is exactly your case.

The change in behavior comes from P2280: Using unknown pointers and references in constant expressions. It was decided with no votes against to treat this as a defect report. This means that your code should also work in C++20 and prior (with a recent compiler).

C++20 legacy wording (obsoleted by defect report)

The C++20 standard [expr.const] p5.12 says that a core constant expression cannot evaluate:

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 usable in constant expressions or
  • its lifetime began within the evaluation of [the core constant expression];

This wording does restrict any reference parameter from being used in constant expression, but the C++23 paper is a defect report and has obsoleted this wording.

Musca answered 15/9, 2023 at 14:42 Comment(0)
S
-2

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.

Seedy answered 10/1, 2019 at 12:15 Comment(6)
What's so unfortunate about a.b evaluating a (or f().b evaluating f())?Elderly
@Elderly When b is a static member, a will never be accessed!Seedy
@Elderly But actualy I have just checked, there is a second design error in the definition of array! size is a non static member! That is crazy! There have been too many modification to the language. The entire design of the STL is obsolete.Seedy
@Elderly 1) Why did the standard added "The postfix expression before the dot or arrow is evaluated"?? The fact a member is static or non-static is known at compile time in every translation unit where it is used. 2) Is there a paragraph in the library part of the standard that gives freedom to implementation to choose whether a member is implemented as a static member or non static one? I can not find any good reason to force this.Seedy
What if for some reason you want to apply & operator to size() in generic code? If it is static, &size will be a function pointer, if it is non-static, it will be a member function pointer. For example, such generic code will not work with both "static-augmented" std::array and std::vector uniformly. Making size() a static member function may break some existing code.Piperine
@Evg: That isn't (and wasn't, and won't be) permitted anyway. eel.is/c++draft/constraints#namespace.std-6 You can use a lambda to bridge the gap between std::array::size() and a function pointer, but you can't apply "operator &"Jacquelynejacquelynn

© 2022 - 2024 — McMap. All rights reserved.