c++arraystemplateslambdaconstexpr

Why a std::array is not constant expression when it is the input of a templated function/generic lambda?


(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?


Solution

  • 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>{});
    }