Текстовое представление элементов scoped enum'а

mrgordonfreman
mrgordonfreman

Нередко приходится иметь дело (особенно в задачах логирования) с представлением элементов перечисления в текстовом виде. Разработчики решают эту задачу по-разному, причем в одном проекте могут сосуществовать несколько способов. Поэтому хочется иметь одно стандартное решение на уровне языка, при этом не забывая о принципе "you don't pay for what you don't use".

Для этого предлагаю расширить объявление scoped enum'а следующим образом:

enum class color: int, char
{
    red,
    green,
    blue
};

//usage
color c = color::blue;
std::cout << static_cast<int>(c) << std::endl;
std::cout << static_cast<char const*>(c) << std::endl;

//output
2
blue

Если за типом элементов перечисления следует символьный тип (char, wchar_t), то необходимо "включить" возможность получения текстового представления с помощью конструкции static_cast. По умолчанию текстовое представление соответствует имени элемента в перечислении.

Явно задать текст можно следующим образом:

enum class color: int, wchar_t
{
    red(L"Red color") = 100,
    green,
    blue(L"Blue color")
};

//usage
color c = color::blue;
std::wcout << static_cast<int>(c) << std::endl;
std::wcout << static_cast<wchar_t const*>(c) << std::endl;

//output
102
Blue color

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

9
рейтинг
11 комментариев
Sir-VestniK
Как быть если в перечислении встречаются одинаковые по значению, но разные по имени элементы?

enum class A {a = 1, b, c = 1};
void foo(A val) {std::cout << static_cast<const char*>(val);}

foo(A::a);
foo(A::c);

На сколько я понимаю генеральную линию партии, то путь к выяснению строкового представления элемент енума лежит через open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0194r2.html
Sir-VestniK
mrgordonfreman
Смысл тут не в том, чтобы просто получить строковое представление элемента как есть. С этим как раз должны справиться последние предложения по рефлексии.
Хочется иметь возможность дополнительно "нагрузить" какое-либо перечисление human-readable текстом.
Одинаковые значения перечисления тут никак не влияют. Мы же имеем дело со strongly typed enum и A::a != A::c
mrgordonfreman
Sir-VestniK
mrgordonfreman, Вообще-то A::a == A::c == static_cast<A>(1); На этапе исполнения данные два значения неотличимы. Аргумент val у функции foo в моём примере это просто int и только компилятор (не сильно жёстко, static_cast<A>(100500) только в C++17 станет UB) проверяет что в этот int складывают только 1 (A::a, A::c) или 2 (A::b).

А по поводу добавления доп информации, может хочется разрешить использовать в качестве underlying-type не только численные типы?

struct named_color {
uint32 rgba;
const char* name;
};
enum class fuxed_palette: named_color {
red = {0xff000000, "Red"},
green = {0x00ff0000, "Green"},
...
};

Помоему это решение способно дать куда больше чем просто добавление строковой метаинформации к элементам перечислений. Ещё в списках рассылки SG7 (подгруппа по рефлексии) несколько раз обсуждали возможность определять свои атрибуты и доступаться до пользовательских атрибутов навешанных на опреледения через статическую рефлексию. Но в первую техническую спецификацию ничего такого включать не хотят.
Sir-VestniK
mrgordonfreman
Sir-VestniK, про A::a == A::c в рантайме все верно, я имел в виду что компилятор их может различить при желании, когда будет генерировать таблицы со строковой информацией. А вот когда будет решать обратную задачу - в рантайме по значению получить строковое представление, то тогда неопределенность возникнет.
В таком случае это скорее проблема пользователя, который завел две сущности с одним значением. Самому потом не отличить их будет. Беда в том, что язык это позволяет.
Мне нравится идея использовать в качестве underlying-type не только целочисленные, а хотя бы еще и POD-типы. Моим страданиям это поможет. Вот только реализовать это крайне проблематично будет) В след за этим придется доработать оператор switch, и разрешить как-нибудь задавать пользовательскую функцию сравнения элементов енума (например, когда вместо memcmp захочется сравнивать по какому-то одному уникальному полю структуры)
mrgordonfreman
Sir-VestniK
mrgordonfreman,
Это не беда что язык позваляет одинаковые значения разным элементам перечисления присваивать. Периодически это бывает очень полезно, например при версионировании форматов (например можно посмотреть как QDataStream::Version эту фичу использует)

А switch на старте не требуется подстраивать

fixed_palette palete_color = ...;
switch (palete_color.rgba) {
case fixed_palette::green.rgba: do_smth1(); break;
case fixed_palette::red.rgba: do_smth2(); break;
...
}

.rgba постоянно может и не очень удобно писать, и ограничения на enum underlying type и ограничения на значения в switch одновременно убрать будет сложней чем по шагам. В целом я обеими руками за использование пользовательских типов имеющих constexpr конструктор в качестве enum underlying type.
Sir-VestniK
mrgordonfreman
Sir-VestniK, есть уже оформленный пропозал на эту тему? Наверняка этим кто-то уже занимался
mrgordonfreman
Sir-VestniK
mrgordonfreman, среди того что я читал в списке groups.google.com/a/isocpp.org/forum/#!forum/std-proposals такого не проскакивало, но там не всё и я подписался и регулярно стал его читать только последние месяца 4. Можно спросить там, за одно и собрать больше конструктивной критики.
Sir-VestniK
yndx-antoshkka
Не получится придумать решение, которое бы устраивало всех. Что делать если значение enum не равно ни одному из заданных значений:

color с = static_cast<color>(777);
color с = red | blue;

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

ostream& operator<<(ostream& os, color c) {
if (c&green) os << "greeny";
switch(c) {
case blue: return os << "blue";
case red: return os << "red color";
}

return os << static_cast<int>(c);
}
yndx-antoshkka
Sir-VestniK
yndx-antoshkka, а как насчёт идеи из обсуждения выше, про возможность использовать любой тип значения которго можно создавать constexpr выражением в качестве underlying type для enum class?

Это бы закрывало и проблему mrgordonfreman'а и открывало бы множество новых возможностей. Например, если нужно взаимодействовать со скриптовыми языками, то возможность использовать string_view или хотя бы const char* в качестве значений элементов перечисления которое должно просовываться в скрипт было бы очень удобным.
Sir-VestniK
mrgordonfreman
yndx-antoshkka, для одного енума можно написать отдельную функцию вывода, но что делать когда таких енумов несколько? Приходится писать много однотипного кода. Иногда люди вообще std::map заводят для хранения строковых констант. Например, у нас в проекте элементы енума должны выводится кирилицей, поэтому элементарная рефлексия не поможет. В обсуждении сошлись на мысли использовать в качестве underlying типа, значения которого можно создавать constexpr выражением.
mrgordonfreman
yndx-antoshkka
> любой тип значения которго можно создавать constexpr выражением в качестве underlying type для enum class?

Это очень интересная мысль. Что если её расширить и требовать, чтобы только *значения* перечисления были constexpr выражениями и не накладывать никаких ограничений на сам класс:

enum class my_string : std::string{}; // strong typedef, OK
enum class alloved_values : std::string_view {
hello = "Hello", // constexpr constructible. OK
empty // constexpr default constructible. OK
};

Дополнительно надо подумать про операторы:
enum class my_int: int {};
my_int i = 10;
my_int j = 11;
i += j; // currently not allowed
yndx-antoshkka
Другие идеи
Группа создана, чтобы собирать предложения к стандарту C++, организовывать их внутренние обсуждения, помогать готовить их для отправки в комитет и защищать на общих собраниях в рабочей группе по С++ Международной организации по стандартизации (ISO).