1

(Realted to this other question of mine; if you give a look at that too, I would really appreciate it.)

If std::array<T,N>::size is constexpr, then why does the following code not even compile?

#include <array>
#include <iostream>

constexpr auto print_size = [](auto const& array){
    constexpr auto size = array.size();
    std::cout << size << '\n';
};

int main() {
    print_size(std::array<int,3>{{1,2,3}});
}

The error is the following:

$ g++ -std=c++17 deleteme.cpp && ./a.out 
deleteme.cpp: In instantiation of ‘<lambda(const auto:1&)> [with auto:1 = std::array<int, 3>]’:
deleteme.cpp:10:42:   required from here
deleteme.cpp:5:20: error: ‘array’ is not a constant expression
    5 |     constexpr auto size = array.size();
      |                    ^~~~

But I wonder why.

At the lambda call site, the argument is known at compile time, and the lambda should be instantiated with auto equal to std::array<int,3>, where 3 is a compile time value, and so should be output of array.size().

What is wrong in my reasoning?

By the way, the same holds if I use a templated function instead of the generic lambda.

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • 2
    [`return array.size();`](https://godbolt.org/z/4dqsYe) works though. – Ted Lyngmo Dec 10 '20 at 14:40
  • @TedLyngmo, this makes the thing even stranger. Proably RVO is bypassing some limitation of the language? – Enlico Dec 10 '20 at 14:46
  • Indeed. `constexpr size = array.size();` inside the lambda won't work, but returning the same and assigning to a `constexpr` variable does. No idea why there's a diff :-) – Ted Lyngmo Dec 10 '20 at 14:49
  • 5
    Function parameters are never `constexpr`. Remember that even a `constexpr` function must be callable at run-time. – super Dec 10 '20 at 14:49
  • @super True, but it does look wierd in a case like this i.m.o. – Ted Lyngmo Dec 10 '20 at 14:56
  • 1
    @TedLyngmo Indeed. I remember seeing a good article/video about this here on SO somewhere. It goes into a lot more depth and explains why this (not sure if this exact scenario, but at least similar) is not even possible with `consteval`. I'll see if I can find it. – super Dec 10 '20 at 14:58
  • 3
    You might use `constexpr auto size = std::tuple_size>::value`. – Jarod42 Dec 10 '20 at 15:02
  • @super, how does Ted Lyngmo's example escape this rule? – Enlico Dec 10 '20 at 15:14
  • @Enlico It doesn't. The return value doesn't have to be `constexpr`. But if it is, we are allowed to use it as one at the calling site. Which in turn depends on the parameter passed in, but at the calling site the compiler knows if the parameter passed in is a `constexpr` or not as opposed to inside the function definition. – super Dec 10 '20 at 15:22
  • But doesn't it know at the time of instantiating the generic lambda/template function? – Enlico Dec 10 '20 at 15:23
  • 1
    Sure. But even an instatiated template must be callable at run-time, so that doesn't really change anything. – super Dec 10 '20 at 15:24
  • Probably I miss some bits. I don't see why the compiler can't instantiate the lambda with `auto` equal to `std::array` and then be able to call it at run time too. It's a `std::array`, it won't change size at run time, no? – Enlico Dec 10 '20 at 15:27
  • @Enlico I'm sure there are duplicates here on SO that goes into more details on this, but I can't seem to find them right now. I can't give you a waterproof answer to that, but I'm 99% sure I've seen it and read it on here somewhere. – super Dec 10 '20 at 15:37
  • @super, I've come back to this while watching the presentation I've now linked off the self-answer I've just written. Now I understand what you meant. Thank you very much. By the way, is this the video you were referring to in your second comment? – Enlico Jan 15 '21 at 18:48

2 Answers2

3

The problem is [expr.const]/5.12:

5 - An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine ([intro.execution]), would evaluate one of the following: [...]

  • (5.12) an id-expression that refers to a variable or data member of reference type unless the reference has a preceding initialization and either
    • (5.12.1) it is usable in constant expressions or
    • (5.12.2) its lifetime began within the evaluation of E;

Since the variable array is a reference, it is not permitted to evaluate it (inside the expression array.size()), even though the evaluation doesn't actually do anything.

Passing array by value (const or non-const) makes the code valid:

constexpr auto print_size = [](auto const array){
    constexpr auto size = array.size(); // ok
    std::cout << size << '\n';
};

But taking a reference to that parameter and using it on the very next line is invalid:

constexpr auto print_size = [](auto const arr){
    auto const& array = arr;
    constexpr auto size = array.size(); // error
    std::cout << size << '\n';
};

Note that gcc 9 incorrectly accepts this code; it is only since version 10 that gcc gets this correct.

gcc 10 still is noncompliant in a related area; it accepts calling a static constexpr member function on a reference. Using a constexpr static member of a reference as template argument This is incorrect and clang correctly rejects it:

struct S { static constexpr int g() { return 1; } };
void f(auto const& s) {
    constexpr auto x = s.g(); // error
    constexpr auto y = decltype(s)::g(); // ok
}
int main() { f(S{}); }

Addendum: this may change in future, per the paper P2280R1.

ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • I'm not so sure this answer is correct. There are no constexpr function parameters so `constexpr auto size = array.size();` should be illegal in all cases. I think the reason it does compile is because the `operator()` is a template (because of `auto`) and an invalid constexpr template does not require any diagnose. – NathanOliver Dec 10 '20 at 17:27
  • @NathanOliver well, clang accepts function (non-template) `void print_size(std::array const array)`: https://godbolt.org/z/zKbYvs – ecatmur Dec 10 '20 at 17:38
  • @NathanOliver I don't see how `array.size()` called on a (non-`constexpr`) value of type `std::array` violates any provision of http://eel.is/c++draft/expr.const#5 ? – ecatmur Dec 10 '20 at 17:40
  • My issues is that a non-constexpr variable, which is what `array` should be, should not be able to return a constexpr value in which to initialize `size` with. I don't understand how it's allowing it to be used. – NathanOliver Dec 10 '20 at 18:01
  • 1
    @NathanOliver `array::size()` is a `constexpr` function that does not access any data members of `array`, and the compiler can see that. It seems fine to me. – ecatmur Dec 10 '20 at 18:16
1

I was watching the 2014 Metaprogramming with Boost.Hana: Unifying Boost.Fusion and Boost.MPL presentation, where Louise Dionne touches this topic and explains what @super was telling me in the comments, but I was not understanding it.

This is my rewording of that concept: there's no such a thing as a constexpr function parameter, therefore whenever the lambda (actually its underlying operator()) is instantiated for a given type of array, that single instantiation is the one that should work both for constexpr and non-constexpr arguments of that type.

As Louis Dionne says in the linked presentation,

[…] you can't generate a constexpr inside a function if it depends on a parameter […] the return type of a function may only depend on the types of its arguments, not on their values […]

This give a way around the issue. Use array's type without using array's value:

constexpr auto print_size = [](auto const& array){
    using array_type = decltype(array);
    constexpr auto size = array_type{}.size();
    std::cout << size << '\n';
};

which I think it's not different, in essence, from what @Jarod42 suggested in a comment:

You might use constexpr auto size = std::tuple_size<std::decay_t<decltype(array)>>::value

As an addition, I played around a bit more, because a last thing was bugging me: the size of std::array is not part of the value, but it's part of the type, so why can't I call size member function in contexprs? The reason is that std::array<T,N>::size() is sadly not static. If it was, one could call it as in the commented line below (the struct A is for comparison):

#include <array>
#include <iostream>
#include <type_traits>

template<std::size_t N>
struct A {
    static constexpr std::size_t size() noexcept { return N; }
};
constexpr auto print_size = [](auto const& array){
    constexpr auto size = std::decay_t<decltype(array)>::size();
    std::cout << size << '\n';
};

int main() {
    //print_size(std::array<int,3>{{1,2,3}});
    print_size(A<3>{});
}
Enlico
  • 23,259
  • 6
  • 48
  • 102