Почему initializer_list значений enum не считается константным выражением? - программирование

Почему initializer_list значений enum не считается константным выражением?

В следующем коде (проверено локально и на Wandbox):

#include <iostream>

enum Types
{
    A, B, C, D
};

void print(std::initializer_list<Types> types)
{
    for (auto type : types)
    {
        std::cout << type << std::endl;
    }
}

int main()
{
    constexpr auto const group1 = { A, D };
    print(group1);
    return 0;
}

MSVC 15.8.5 не компилируется с:

error C2131: expression did not evaluate to a constant
note: failure was caused by a read of a variable outside its lifetime
note: see usage of '$S1'

(все ссылаются на строку, содержащую constexpr)

Clang 8 (HEAD) сообщает:

error: constexpr variable 'group1' must be initialized by a constant expression
    constexpr auto const group1 = { A, D };
                         ^        ~~~~~~~~
note: pointer to subobject of temporary is not a constant expression
note: temporary created here
    constexpr auto const group1 = { A, D };
                                  ^

GCC 9 (HEAD) сообщает:

In function 'int main()':
error: 'const std::initializer_list<const Types>{((const Types*)(&<anonymous>)), 2}' is not a constant expression
   18 |     constexpr auto const group1 = { A, D };
      |                                          ^
error: could not convert 'group1' from 'initializer_list<const Types>' to 'initializer_list<Types>'
   19 |     print(group1);
      |           ^~~~~~
      |           |
      |           initializer_list<const Types>

Зачем?

Во-первых, все они, по-видимому, считают enum-идентификаторы непостоянными, несмотря на то, что они, очевидно, являются общеизвестными постоянными значениями времени компиляции.

Во- вторых, MSVC жалуется чтения за пределами жизни, но время жизни group1 и его значения должны распространяться по всей его использования в print.

В-третьих, у gcc есть странная жалоба const-vs-non-const, которую я не могу понять, поскольку списки инициализаторов всегда являются const.

Наконец, все, кроме gcc, с радостью скомпилируют и запустят этот код без проблем, если будет удален constexpr. Конечно, в этом нет необходимости, но я не вижу веских причин, чтобы это не сработало.

Между тем, gcc будет компилировать и запускать код только в том случае, если тип параметра будет изменен на std::initializer_list<const Types> - и внесение этого изменения приведет к сбою компиляции как в MSVC, так и в clang.

(Интересно: gcc 8, с изменением типа параметра, успешно компилирует и запускает код, включая constexpr, где constexpr ошибки gcc 9.)


FWIW, изменив объявление на это:

    constexpr auto const group1 = std::array<Types, 2>{ A, D };

Компилирует и запускает все три компилятора. Таким образом, вероятно, что сам initializer_list неправильно работает, а не перечисляет значения. Но синтаксис более раздражает. (Это немного менее раздражает при подходящей реализации make_array, но я до сих пор не понимаю, почему оригинал недействителен.)


    constexpr auto const group1 = std::array{ A, D };

Также работает, благодаря введению шаблона С++ 17. Хотя теперь print не может взять initializer_list; он должен быть основан на общей концепции контейнера/итератора, что неудобно.

4b9b3361

Ответ 1

Когда вы инициализируете std::initializer_list это происходит так:

[dcl.init.list] (выделено мной)

5 Объект типа std :: initializer_list создается из списка инициализаторов, как если бы реализация генерировала и материализовала значение типа "массив из N const E", где N - количество элементов в списке инициализаторов. Каждый элемент этого массива инициализируется копией с соответствующим элементом списка инициализатора, и объект std :: initializer_list создается для ссылки на этот массив. [Примечание: конструктор или функция преобразования, выбранные для копии, должны быть доступны в контексте списка инициализатора. - примечание к концу] Если для инициализации какого-либо элемента требуется сужающее преобразование, программа имеет некорректную форму. [ Пример:

struct X {
  X(std::initializer_list<double> v);
};
X x{ 1,2,3 };

Инициализация будет реализована примерно таким образом:

const double __a[3] = {double{1}, double{2}, double{3}};
X x(std::initializer_list<double>(__a, __a+3));

Предполагая, что реализация может создать объект initializer_list с парой указателей. - конец примера]

То, как этот временный массив используется для инициализации std::initializer_list определяет, initializer_list initializer_list константным выражением. В конечном счете, в соответствии с примером (несмотря на то, что он ненормативный), эта инициализация будет принимать адрес массива или его первого элемента, который будет производить значение типа указателя. И это не является допустимым константным выражением.

[expr.const] (выделение мое)

5 Константное выражение - это либо ключевое постоянное выражение glvalue, которое относится к объекту, являющемуся разрешенным результатом постоянного выражения (как определено ниже), либо базовое постоянное выражение prvalue, значение которого удовлетворяет следующим ограничениям:

  • если значение является объектом типа класса, каждый нестатический член данных ссылочного типа относится к объекту, который является разрешенным результатом константного выражения,
  • если значение имеет тип указателя, оно содержит адрес объекта со статической продолжительностью хранения, адрес после конца такого объекта ([expr.add]), адрес функции или значение нулевого указателя и
  • если значение является объектом класса или типа массива, каждый подобъект удовлетворяет этим ограничениям для значения.

Сущность - это разрешенный результат константного выражения, если это объект со статической продолжительностью хранения, который либо не является временным объектом, либо является временным объектом, значение которого удовлетворяет вышеуказанным ограничениям, либо является функцией.

Однако если массив был статическим объектом, то этот инициализатор представлял бы собой допустимое константное выражение, которое можно использовать для инициализации объекта constexpr. Так как std::initializer_list влияет на время жизни на этот временный объект [dcl.init.list]/6, когда вы объявляете group1 как статический объект, clang и gcc, кажется, выделяют массив также как статический объект, что делает Правильность инициализации зависит только от того, является ли std::initializer_list литеральным типом, а используемый конструктор - constexpr.

В конечном счете, все это немного мутно.

Ответ 2

Похоже, что std::initializer_list еще не (в С++ 17) удовлетворяет требованиям литерального типа (что является требованием, constexpr должен удовлетворять constexpr переменной constexpr).

Обсуждение того, делает ли это в С++ 14, можно найти в этом посте: почему std::initializer_list определен как литеральный тип? что само по себе было продолжением поста, обсуждающего вопрос: законно ли объявлять объект constexpr initializer_list?

Я сравнил цитаты, приведенные в соответствующем посте С++ 14 (стандарта С++ 14), с окончательным рабочим проектом (стандарта С++ 17), и они совпадают. Поэтому нет явного требования, чтобы std::initializer_list был литеральным типом.

Цитаты из окончательного рабочего проекта С++ 17 (n4659):

[basic.types]/10.5

(10.5) возможно cv-квалифицированный тип класса (раздел 12), который имеет все следующие свойства:
(10.5.1) - у него есть тривиальный деструктор,
(10.5.2) - это либо тип замыкания (8.1.5.1), либо агрегатный тип (11.6.1), либо он имеет хотя бы один конструктор или шаблон конструктора constexpr (возможно, унаследованный (10.3.3) от базового класса) это не конструктор копирования или перемещения,
(10.5.3) - если это объединение, то по крайней мере один из его нестатических элементов данных имеет тип энергонезависимого литерала, и
(10.5.4) - если он не является объединением, все его нестатические члены-данные и базовые классы имеют энергонезависимые литеральные типы.

[initializer_list.syn]/1

  1. Объект типа initializer_list обеспечивает доступ к массиву объектов типа const E. [Примечание: пара указателей или указатель плюс длина были бы очевидными представлениями для initializer_list. initializer_list используется для реализации списков инициализаторов, как указано в 11.6.4. Копирование списка инициализатора не копирует базовые элементы. —Конечная записка]

По этой причине нельзя объявить constexpr initializer_list.