Перечисление с незаданной областью, перечислитель и неопределенность базового типа в C ++ - PullRequest
0 голосов
/ 21 января 2019

Я проходил через стандарт C ++ n4713.pdf. Рассмотрим ниже код:

#include <iostream>
#include <type_traits>

enum UEn
{
    EN_0,
    EN_1,
    EN_L = 0x7FFFFFFFFFFFFFFF            // EN_L has type "long int"
};                                       // UEn has underlying type "unsigned long int"

int main()
{
    long lng = 0x7FFFFFFFFFFFFFFF;

    std::cout << std::boolalpha;
    std::cout << "typeof(unsigned long == UEn):" << std::is_same<unsigned long, std::underlying_type<UEn>::type>::value << std::endl;  // Outputs "true"
    std::cout << "sizeof(EN_L):" << sizeof(EN_L) << std::endl;
    std::cout << "sizeof(unsigned):" << sizeof(unsigned) << std::endl;
    std::cout << "sizeof(unsigned long):" << sizeof(unsigned long) << std::endl;
    std::cout << "sizeof(unsigned long):" << sizeof(unsigned long long) << std::endl;

    lng = EN_L + 1;                      // Invokes UB as EN_L is 0x7FFFFFFFFFFFFFFF and has type "long int"

    return 0;
}

Вышеприведенный код выводится (проверено на g ++ - 8.1, Clang):

typeof(unsigned long == UEn):true sizeof(EN_L):8 sizeof(unsigned):4 sizeof(unsigned long):8 sizeof(unsigned long):8

Согласно разделу 10.2p5 (10.2 декларации перечисления):

После закрывающей скобки спецификатора перечисления каждый перечислитель имеет тип его перечисления ... Если базовый тип не фиксирован, Тип каждого перечислителя перед закрывающей скобкой определяется как следующим образом:

  • Если для перечислителя указан инициализатор, Выражение константы должно быть интегральным выражением константы (8.6). Если выражение имеет тип перечисления с незаданной областью, перечислитель имеет базовый тип этого типа перечисления, в противном случае он имеет тот же введите в качестве выражения.

  • Если инициализатор не указан для первого перечислитель, его тип - неопределенный целочисленный тип со знаком.

  • В противном случае тип счетчика такой же, как и у предыдущий перечислитель, если увеличенное значение не представимо в этом типе, и в этом случае тип является неопределенным целочисленным типом достаточно, чтобы содержать увеличенное значение. Если такого типа не существует, программа некорректна.

Далее, в разделе 10.2p7 говорится:

Для перечисления, базовый тип которого не фиксирован, базовый тип является целочисленным типом, который может представлять все значения перечислителя определено в перечислении. Если ни один целочисленный тип не может представлять все значения перечислителя, перечисление является некорректным. это определенный реализацией, какой интегральный тип используется в качестве базового тип за исключением того, что базовый тип не должен быть больше, чем int если значение перечислителя не может поместиться в int или unsigned внутр.


Таким образом, у меня есть следующие вопросы:

  1. Почему базовый тип перечисления UEn и unsigned long, когда 0x7FFFFFFFFFFFFFFF является целочисленной константой типа long int и, следовательно, тип EN_L также равен long int. Это ошибка компилятора или четко определенное поведение?
  2. Когда стандарт гласит each enumerator has the type of its enumeration, не должно ли это означать, что целочисленные типы перечислителя и перечисления также должны совпадать? В чем может быть причина того, что они отличаются друг от друга?

Ответы [ 3 ]

0 голосов
/ 21 января 2019

Я полагаю, что ответ на это (по общему признанию, не интуитивное) предупреждение в 7.6 Интегральные продвижения [conv.prom]:

Значение типа перечисления с незаданной областью, базовый тип которого не является фиксированный (10.2) может быть преобразован в значение первого из следующие типы, которые могут представлять все значения перечисления (то есть значения в диапазоне от b min до b max , как описано в 10.2): int, unsigned int, long int, unsigned long int, long long int или unsigned long long int.

Т.е., если ваш базовый тип не является фиксированным, и вы используете член перечисления в выражении, он не обязательно преобразуется в базовый тип перечисления. Вместо этого он преобразуется в первый тип в этом списке, в который помещаются все элементы.

Не спрашивайте меня, почему, правило кажется мне чокнутым.

В этом разделе говорится:

Значение типа перечисления с незаданной областью, базовый тип которого fixed (10.2) может быть преобразован в prvalue его базового типа.

т.е. если вы исправите базовый тип с помощью unsigned long:

enum UEn : unsigned long
...

тогда предупреждение исчезнет.

Еще один способ избавиться от предупреждения (и оставить базовый тип без изменений) - добавить элемент, который требует unsigned long хранилища:

EN_2 = 0x8000000000000000

Опять же, предупреждение исчезает.

Хороший вопрос. Я многому научился, отвечая на него.

0 голосов
/ 21 января 2019

Формулировка раздела 10.2p5 прямо гласит: «... до закрывающей скобки ...» предлагает следующую интерпретацию.Тип перечислителя в определении типа перечисления (до закрывающей скобки) выбирается как некоторый целочисленный тип, достаточно большой для представления его значения.Это значение затем может быть повторно использовано в определении последующего определения перечислителей в том же перечислении.Когда встречается закрывающая скобка типа enum, компилятор выбирает достаточно большой целочисленный тип для представления всех значений перечислителя.После определения типа перечисления все значения перечислителя имеют один и тот же тип (который является типом перечисления) и совместно используют базовый тип перечисления.Например:

#include <iostream>
#include <typeinfo>
#include <type_traits>

enum E1
{
  e1 = 0, // type of the initializer (int), value = 0
  e2 = e1 + 1U, // type of the initializer (unsigned = int + unsigned), value = 1U
  e3 = e1 - 1, // type of the initializer (int = int - int), value = -1
}; // range of values [-1, 1], underlying type is int

int main()
{
   std::cout << typeid(std::underlying_type<E1>::type).name() << '\n';
   std::cout << typeid(e1).name() << '\n';
   std::cout << typeid(e2).name() << '\n';
   std::cout << typeid(e3).name() << '\n';
}

Ран с clan5 и gcc8 и выводит:

i
2E1
2E1
2E1
0 голосов
/ 21 января 2019

Базовый тип определяется реализацией.Он должен иметь возможность представлять каждый перечислитель и не может быть больше int, если не требуется.Не существует требования в отношении подписи (за исключением того, что базовый тип должен иметь возможность представлять каждый перечислитель), для dcl.enum.7 , как вы уже нашли.Это ограничивает обратное распространение типов перечислителей больше, чем вы предполагаете.Примечательно, что нигде не говорится, что базовый тип перечисления должен быть типом любого из инициализаторов перечислителей.

Clang предпочитает целые числа без знака как основы перечисления над целыми числами со знаком;это все, что нужно сделать.Важно отметить, что тип перечисления не должен соответствовать какому-либо конкретному типу перечислителя: он должен только представлять каждого перечислителя.Это вполне нормально и понятно в других контекстах.Например, если у вас есть EN_1 = 1, вас не удивит, что базовый тип перечисления не int или unsigned int, даже если 1 является int.

Вы такжеправильно сказать, что тип 0x7fffffffffffffff равен long.Clang согласен с вами, однако он неявно преобразует константу в unsigned long:

TranslationUnitDecl
`-EnumDecl <line:1:1, line:5:1> line:1:6 Foo
  |-EnumConstantDecl <line:2:5> col:5 Frob 'Foo'
  |-EnumConstantDecl <line:3:5> col:5 Bar 'Foo'
  `-EnumConstantDecl <line:4:5, col:11> col:5 Baz 'Foo'
    `-ImplicitCastExpr <col:11> 'unsigned long' <IntegralCast>
      `-IntegerLiteral <col:11> 'long' 576460752303423487

Это разрешено, поскольку, как мы уже говорили, базовый тип перечисления не нуждается вбыть дословным типом любого перечислителя.

Когда стандарт говорит, что каждый перечислитель имеет тип перечисления, это означает, что тип EN_1 равен enum UEn после закрывающей скобки перечисления.Обратите внимание, что «после закрывающей скобки» и «до закрывающей скобки» упоминаются.

Перед закрывающей скобкой, если перечисление не имеет фиксированного типа, тогда тип каждого перечислителя - это тип его инициализирующего выражениятипа, но это только временно.Это то, что позволяет, например, писать EN_2 = EN_1 + 1 без приведения EN_1 даже в области enum class.Это больше не верно после закрывающей скобки.Вы можете заставить компилятор показывать вам, просматривая сообщения об ошибках или просматривая разборку:

template<typename T>
T tell_me(const T&& value);

enum Foo {
    Baz = 0x7ffffffffffffff,
    Frob = tell_me(Baz)
    // non-constexpr function 'tell_me<long>' cannot be used in a constant expression
};

Обратите внимание, что в этом случае T было определено как long, но после закрывающей скобки этоПредполагается, что Foo:

template<typename T>
T tell_me(const T&& value);

enum Foo {
    Baz = 0x7ffffffffffffff
};

int main() {
    tell_me(Baz);
    // call    Foo tell_me<Foo>(Foo const&&)
}

Если вы хотите, чтобы ваш тип перечисления был подписан с помощью Clang, вам нужно указать его с использованием синтаксиса : base_type, или вам нужен отрицательный перечислитель.

...