When can a std::initializer_list be constexpr?
Asked Answered
E

1

8

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;
}
  • "gcc" means x86-64 gcc (trunk) with -std=c++20 (also tested with 13.1)
  • "clang" means x86-64 clang (trunk) with -std=c++20 (also tested with 16.0.0)
  • "msvc" means x64 msvc v19.latest with /std:c++20

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.

Ergocalciferol answered 21/7, 2023 at 17:45 Comment(6)
Foo is not marked as constexpr...Phytohormone
Clang's error about no viable deduction guide for initializer_list is a bug, which is tracked in issues/60876.Surplice
constexpr auto the_list = std::initializer_list{1, 2, 3}; -> constexpr static auto the_list = std::initializer_list{1, 2, 3};Fenrir
Isn't std::initializer_list a literal type?Busywork
@Busywork I'm not actually sure. If you create one on the stack, it might be ephemeral.Fenrir
@Jarod42: Oops. I had marked Foo as constexpr at one point, but I mucked it up while putting the question together. Thanks!Ergocalciferol
A
5

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.

Algicide answered 21/7, 2023 at 18:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.