8.2 – Перечислимые типы данных

Добавлено 2 июня 2021 в 19:29

C++ содержит довольно много встроенных типов данных. Но этих типов бывает не всегда достаточно для того, что мы хотим сделать. Итак, C++ содержит возможности, позволяющие программистам создавать свои собственные типы данных. Эти типы данных называются пользовательскими типами данных.

Возможно, самый простой пользовательский тип данных – это перечислимый тип. Перечислимый тип (также называемый перечислением или enumeration, enum) – это тип данных, в котором каждое возможное значение определяется как символьная константа (называемая перечислителем, enumerator). Перечисления определяются с помощью ключевого слова enum. Давайте посмотрим на пример:

// Определение нового перечисления с именем Color
enum Color
{
    // Это перечислители
    // Они определяют все возможные значения, которые этот тип может содержать
    // Каждый перечислитель отделяется запятой, а не точкой с запятой
    color_black,
    color_red,
    color_blue,
    color_green,
    color_white,
    color_cyan,
    color_yellow,
    color_magenta, // после последнего перечислителя может быть запятая, но она не обязательна
}; // однако само перечисление должно заканчиваться точкой с запятой
 
// Определяем несколько переменных перечислимого типа Color
Color paint = color_white;
Color house(color_blue);
Color apple { color_red };

Определение перечисления (или любого пользовательского типа данных) не занимает никакой памяти. Когда определяется переменная перечислимого типа (например, переменная paint в приведенном выше примере), для этой переменной память выделяется.

Обратите внимание, что каждый перечислитель отделяется запятой, а всё перечисление заканчивается точкой с запятой.

Именование перечислений и перечислителей

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

Перечислителям должны быть даны имена, и они обычно используют тот же стиль именования, что и константные переменные. Иногда перечислителям даются имена ПОЛНОСТЬЮ_ЗАГЛАВНЫМИ_БУКВАМИ, но делать это не рекомендуется, поскольку это может привести к путанице с именами макросов препроцессора.

Область видимости перечислителя

Поскольку перечислители помещаются в то же пространство имен, что и перечисление, имя перечислителя нельзя использовать в нескольких перечислениях в одном пространстве имен:

enum Color
{
  red,
  blue, // blue помещается в глобальное пространство имен
  green
};
 
enum Feeling
{
  happy,
  tired,
  blue // ошибка, blue уже использовался в enum Color в глобальном пространстве имен
};

Следовательно, для предотвращения конфликтов имен и для документирования кода в именах перечислителей обычно используются префиксы, например, animal_ или color_.

Значения перечислителей

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

enum Color
{
    color_black,  // присвоено значение 0
    color_red,    // присвоено значение 1
    color_blue,   // присвоено значение 2
    color_green,  // присвоено значение 3
    color_white,  // присвоено значение 4
    color_cyan,   // присвоено значение 5
    color_yellow, // присвоено значение 6
    color_magenta // присвоено значение 7
};
 
Color paint{ color_white };
std::cout << paint;

Приведенная выше инструкция с cout выводит значение 4.

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

// определяем новое перечисление с именем Animal
enum Animal
{
    animal_cat = -3,
    animal_dog, // присвоено -2
    animal_pig, // присвоено -1
    animal_horse = 5,
    animal_giraffe = 5, // имеет то же значение, что и animal_horse
    animal_chicken // присвоено 6
};

Обратите внимание, что в этом случае animal_horse и animal_giraffe имеют одинаковые значения. Когда это происходит, перечислители становятся неразличимыми – по сути, animal_horse и animal_giraffe взаимозаменяемы. Хотя C++ позволяет это, обычно следует избегать присвоения одного и того же значения двум перечислителям в одном перечислении.

Лучшая практика


Не назначайте перечислителям конкретные значения.

Лучшая практика


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

Вычисление перечислимого значения и ввод/вывод

Поскольку перечислимые значения вычисляются как целочисленные значения типа int, они могут быть присвоены целочисленным переменным. Это означает, что они также могут выводиться (как целочисленные значения), поскольку std::cout знает, как выводить целые числа.

int mypet{ animal_pig };
std::cout << animal_horse; // перед передачей в std::cout вычисляется как int

Это дает следующий результат:

5

Компилятор не будет неявно преобразовывать целочисленное значение int в перечислимое значение. Следующее приведет к ошибке компиляции:

Animal animal{ 5 }; // вызовет ошибку компилятора

Однако вы можете заставить его сделать это с помощью static_cast:

auto color{ static_cast<Color>(5) }; // некрасиво

Компилятор также не позволит вам выполнить ввод перечисления с помощью std::cin:

enum Color
{
    color_black,  // присвоено 0
    color_red,    // присвоено 1
    color_blue,   // присвоено 2
    color_green,  // присвоено 3
    color_white,  // присвоено 4
    color_cyan,   // присвоено 5
    color_yellow, // присвоено 6
    color_magenta // присвоено 7
};
 
Color color{};
std::cin >> color; // вызовет ошибку компилятора

Один из способов решения проблемы – прочитать значение int и использовать static_cast, чтобы заставить компилятор поместить целочисленное значение в перечислимый тип:

int inputColor{};
std::cin >> inputColor;
 
auto color{ static_cast<Color>(inputColor) };

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

Animal animal{ color_blue }; // вызовет ошибку компиляции

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

// Использовать в качестве базы для перечисления
// 8-битный целочисленный тип без знака.
enum Color : std::uint_least8_t
{
    color_black,
    color_red,
    // ...
};

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

enum Color;       // Ошибка
enum Color : int; // Okay
 
// ...
 
// Поскольку Color был предварительно объявлен с фиксированной базой,
// нам необходимо снова указать базу и в определении.
enum Color : int
{
    color_black,
    color_red,
    // ...
};

Как и в случае с константными переменными, перечислимые типы отображаются в отладчике, что делает их более полезными в этом отношении, чем значения определенные с помощью #define.

Печать перечислителей

Как вы видели выше, попытка напечатать перечислимое значение с помощью std::cout приводит к печати целочисленного значения перечислителя. Так как же распечатать сам перечислитель в виде текста? Один из способов сделать это – написать функцию и использовать оператор if или switch:

enum Color
{
    color_black,  // присвоено 0
    color_red,    // присвоено 1
    color_blue,   // присвоено 2
    color_green,  // присвоено 3
    color_white,  // присвоено 4
    color_cyan,   // присвоено 5
    color_yellow, // присвоено 6
    color_magenta // присвоено 7
};
 
void printColor(Color color)
{
    switch (color)
    {
    case color_black:
        std::cout << "Black";
        break;
    case color_red:
        std::cout << "Red";
        break;
    case color_blue:
        std::cout << "Blue";
        break;
    case color_green:
        std::cout << "Green";
        break;
    case color_white:
        std::cout << "White";
        break;
    case color_cyan:
        std::cout << "Cyan";
        break;
    case color_yellow:
        std::cout << "Yellow";
        break;
    case color_magenta:
        std::cout << "Magenta";
        break;
    default:
        std::cout << "Who knows!";
    }
}

Выделение памяти для переменных типа enum и предварительное объявление

Типы enum считаются частью целочисленного семейства типов, и компилятор должен определить, сколько памяти выделить для переменной enum. Стандарт C++ говорит, что размер перечисления должен быть достаточно большим, чтобы представлять значения всех перечислителей. Чаще всего он делает переменные перечисления того же размера, что и стандартный тип int.

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

Чем полезны перечислители?

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

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

int readFileContents()
{
    if (!openFile())
        return -1;
    if (!readFile())
        return -2;
    if (!parseFile())
        return -3;
 
    return 0; // успех
}

Однако использование подобных магических чисел не очень наглядно. Альтернативный метод – использование перечислимого типа:

enum ParseResult
{
    // для наших перечислителей нам не нужны конкретные значения
    success,
    error_opening_file,
    error_reading_file,
    error_parsing_file
};
 
ParseResult readFileContents()
{
    if (!openFile())
        return error_opening_file;
    if (!readFile())
        return error_reading_file;
    if (!parsefile())
        return error_parsing_file;
 
    return success;
}

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

if (readFileContents() == success)
{
    // сделать что-то
}
else
{
    // вывести сообщение об ошибке
}

Перечислимые типы лучше всего использовать при определении набора связанных идентификаторов. Например, предположим, что вы пишете игру, в которой игрок может носить один предмет, но этот предмет может быть нескольких разных типов. Вы могли бы сделать это так:

#include <iostream>
#include <string>
 
enum ItemType
{
    itemtype_sword,
    itemtype_torch,
    itemtype_potion
};
 
std::string getItemName(ItemType itemType)
{
    switch (itemType)
    {
      case itemtype_sword:
        return "Sword";
      case itemtype_torch:
        return "Torch";
      case itemtype_potion:
        return "Potion";
    }
 
    // На всякий случай, если в будущем добавим новый элемент и забудем обновить эту функцию
    return "???";
}
 
int main()
{
    // ItemType - это перечислимый тип, который мы определили выше.
    // itemType (i в нижнем регистре) - это имя нашей переменной (типа ItemType).
    // itemtype_torch - это перечислимое значение, которым мы инициализируем переменную itemType.
    ItemType itemType{ itemtype_torch };
 
    std::cout << "You are carrying a " << getItemName(itemType) << '\n';
 
    return 0;
}

Или, в качестве альтернативы, если бы вы писали функцию для сортировки кучи значений:

enum SortType
{
    sorttype_forward,
    sorttype_backwards
};
 
void sortData(SortType type)
{
    if (type == sorttype_forward)
        // сортируем данные в прямом порядке
    else if (type == sorttype_backwards)
        // сортируем данные в обратном порядке
}

Многие языки используют перечисления для определения логических значений. Логическое значение – это, по сути, просто перечисление с двумя перечислителями: false и true! Однако в C++ значения true и false определяются как ключевые слова, а не как перечислители.

Небольшой тест

Вопрос 1

Определите перечислимый тип, чтобы выбрать одну из следующих рас монстров: орки, гоблины, тролли, огры и скелеты.

enum MonsterType
{
    monster_orc,
    monster_goblin,
    monster_troll,
    monster_ogre,
    monster_skeleton
};

Вопрос 2

Определите переменную перечислимого типа, который вы определили в ответе на вопрос 1, и инициализируйте ее с помощью перечислителя троллей.

auto monsterType{ MonsterType::monster_troll }; // Тип очевиден, можно использовать auto.
// Мы использовали префикс MonsterType:: для согласованности со следующим уроком.

Вопрос 3

Верно или нет. Перечислителями могут быть:

  1. заданное целочисленное значение
  2. неприсвоенное значение
  3. заданное значение с плавающей запятой
  4. отрицательное значение
  5. неуникальное значение
  6. значение, инициализированное значением предыдущего перечислителя (например, color_magenta = color_red)

  1. Верно.
  2. Верно. Перечислителям, которым не присвоено значение, будет неявно присвоено целочисленное значение предыдущего перечислителя + 1. Если предыдущего перечислителя нет, перечислитель примет значение 0.
  3. Неверно.
  4. Верно.
  5. Верно.
  6. Верно. Поскольку перечислители вычисляются как целые числа, а целые числа могут быть присвоены перечислителям, перечислители могут быть инициализированы другими перечислителями (хотя обычно для этого нет особых причин!).

Теги

C++ / Cppenum / ПеречислениеLearnCppДля начинающихОбучениеПеречислениеПеречисляемые типы данныхПрограммированиеТипы данных

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.