c++language-lawyerc++20constexprstdinitializerlist

When can a std::initializer_list be constexpr?


According to cppreference.com, std::initializer_lists have constexpr constructors and constexpr size methods (since C++14).

Although the compiler I was using seemed to agree that the size of a constexpr initializer list was indeed constexpr, in some circumstances it didn't believe my list was constexpr. Since std::initializer_lists can involve some "compiler magic," I began wondering whether constexpr doesn't apply to them in quite the same way as it does for non-magical objects.

I hopped onto Compiler Explorer and discovered that the major compilers don't agree on the topic.

So what are the correct behaviors (per the standard) for the four cases below?

#include <initializer_list>

using size_type = std::initializer_list<int>::size_type;

template <typename T>
size_type Foo(std::initializer_list<T> const &list) {
    return list.size();
}

int main() {
    // 1.  Example based on
    // https://en.cppreference.com/w/cpp/utility/initializer_list/size
    // gcc: works
    // clang: no viable c'tor or deduction guide
    // msvc: works
    static_assert(std::initializer_list{1, 2, 3}.size() == 3);

    // 2.  Make a constexpr std::initializer_list<T> with T deduced
    // gcc: not constant expression
    // clang: no viable c'tor or deduction guide
    // msvc: works
    constexpr auto the_list = std::initializer_list{1, 2, 3};

    // 3.  Static assert using constexpr size
    // gcc: fails because of above
    // clang: fails because of above
    // msvc: works
    static_assert(the_list.size() == 3);

    // 4.  Extract the size via a constexpr function
    // gcc: fails because of above
    // clang: fails because of above
    // msvc: expression did not evaluate to a constant
    constexpr auto the_list_size = Foo(the_list);
    return 0;
}

I would have expected all four cases to compile. But some compilers rejected some of them, causing me to reconsider my understanding. Are the compilers rejecting correct code (or accepting incorrect code)? Am I inadvertently relying on implementation-defined behavior? Is the standard ambiguous?


The closest existing question I can find is Assigning a initializer_list to std::array, but the details there are specific to std::array, which is not the case in my examples.


Solution

  • std::initializer_list{1, 2, 3} being invalid is a clang bug (it can't deduce <int> properly. Replacing it with std::initializer_list<int>{1, 2, 3} leads to the same behaviour as gcc. Or you can skirt around this with auto the_list = { 1, 2, 3 };.

    The problem with constexpr auto the_list = std::initializer_list<int>{1, 2, 3}; is the same problem with constexpr const int& the_number = 123;. Like with const int&, std::initializer_list binds to a temporary (in this case, an array). Also like const int&, that temporary's lifetime is extended to the lifetime of the initializer_list variable.

    If a constexpr variable binds to a temporary, that temporary is required to have static storage duration. So, static constexpr auto the_list = std::initializer_list<int>{1, 2, 3}; works (and so does static constexpr const int& the_number = 123;).

    [dcl.init.list]p5

    An object of type std​::​initializer_­list<E> is constructed from an initializer list as if the implementation generated and materialized ([conv.rval]) a prvalue of type “array of N const E”, [...], and the std​::​initializer_­list<E> object is constructed to refer to that array. [...]

    [dcl.init.list]p6

    The array has the same lifetime as any other temporary object ([class.temporary]), except that initializing an initializer_­list object from the array extends the lifetime of the array exactly like binding a reference to a temporary.

    (e.g., auto the_list = { 1, 2, 3 };, the const int[3] has automatic storage duration, but static auto the_list = { 1, 2, 3 };, the const int[3] has static storage duration)

    [expr.const]p11:

    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, [...]


    And your last problem was Foo not being a constexpr function. The fixed version:

    #include <initializer_list>
    
    using size_type = std::initializer_list<int>::size_type;
    
    template <typename T>
    constexpr size_type Foo(std::initializer_list<T> const &list) {
        return list.size();
    }
    
    int main() {
        static_assert(std::initializer_list<int>{1, 2, 3}.size() == 3);
    
        static constexpr auto the_list = {1, 2, 3};
    
        static_assert(the_list.size() == 3);
    
        constexpr auto the_list_size = Foo(the_list);
        return 0;
    }
    

    compiles on the three given compilers. MSVC was too lenient before about the lifetime of the temporary array.