За прошедшие годы я видел множество тем о том, как можно обернуть перечисление или создать шаблон класса с помощью перечисления, чтобы добавить дополнительную функциональность.
Рассмотрим вариант использования. У меня есть перечисление. Я хочу иметь возможность создавать объект класса вокруг класса перечисления, чтобы я мог
Простой способ сделать это
enum class color : uint16_t {
unknown = 0,
red,
green,
blue,
};
class colorClass {
public:
colorClass(){ val = color::unknown; }
colorClass(color v)
:val(v)
{}
//copy, move, assignment ctors section
bool isRed(){ return val == color::red; }
// more such functions
std::string toString() {
using enum color;
switch (val) {
case red : return "red";
case blue: return "blue";
case green: return "green";
case unknown: [[fallthrough]];
default: return "unknown";
}
}
private:
color val;
};
Это работает отлично. Теперь, если я хочу еще немного ограничить этот класс, я могу использовать шаблоны и концепции.
enum class color : uint16_t {
unknown = 0
red,
green,
blue,
};
template<typename E>
concept ValidColor = std::is_enum_v<E>;
template<ValidColor E>
class colorClass {
// Pretty much the same implementation?
};
Моя память немного размыта, но я думаю, что видел несколько умных реализаций toString с использованием базового типа. Я не собираюсь использовать какие-либо сторонние библиотеки.
У меня есть «современный» путь, верно? Помимо принудительного использования перечисления, дает ли добавление шаблонов какие-либо преимущества?
Обновлено: я спрашиваю не только об размышлениях (и спасибо за все ответы в этом направлении), но и о том, как C++20 и C++23 могут улучшить этот класс "перечисления с помощью нескольких трюков".
Если ваш новый код не компилируется, вам следует включить минимальный воспроизводимый пример с полным сообщением об ошибке компилятора.
Вы можете назначать имена перечислений и строки, не повторяя их, используя X-макросы, но это, конечно, не современный способ — он работает даже в чистом C.
В ваших примерах все методы colorClass
являются частными, а метод toString
не принимает нулевые аргументы.
Загрузите Magic Enum с github. Задача решена. (Если только ваша проблема не в абсолютной переносимости на любой совместимый компилятор, который только можно вообразить).
Обязательно прочитайте P2996: отражение для C++26, чтобы увидеть, чего можно ожидать в будущем, и убедиться, что то, что вы делаете сейчас, не слишком далеко отходит от интерфейса, который вы предоставите, когда в C++ будет статическое отражение этого. добрый.
@NathanOliver Я исправил код, спасибо за вклад
Есть рабочий трюк для случаев с максимальным значением перечисления 0x8000
или около того (ограничение ICE для std::make_index_sequence
). Затем вы можете сопоставить строки со значениями, используя массив, заполненный элементами std::source_location
. Затем вам нужно обрезать строки из-за искажения имен, зависящих от платформы:
template<typename enm>
requires std::is_enum_v<enm>
constexpr std::string_view enum_raw_name()
{return std::source_location::current().function_name();};
constexpr std::string_view enum_demangle_trim(std::string_view);//has platform specific implementation
template<typename enm>
requires std::is_enum_v<enm>
constexpr std::pair enum_bounds{
std::to_underlying(enm{}),
std::numeric_limits<std::underlying_t<enm>>::max()
};
template<typename enm>
requires std::is_enum_v<enm>
constexpr std::string_view enum_value_name(enm const e, std::underlying_type_t<enm> u = {}) {
using int_t = decltype(u);
constexpr auto bounds = enum_bounds<enm>;
auto constexpr range = std::min(0x8000uz,std::size_t{bounds.second}-bounds.first);
static auto constexpr map = []<std::size_t ... i>(std::index_sequence<i ...>){
return std::array{
enum_demangle_trim(enum_raw_name<static_cast<enm>(int_t{i} + bounds.first)>())
..., "out of range"};
}(std::make_index_sequence<range>{});
return map[min(std::size_t{int_t{e}}-bounds.first, range)];
};
Ограничениями вышеприведенной реализации являются максимальное значение типа перечисления и отрицательные значения перечисления, которые считаются выходящими за пределы. Метапрограммирование также может включать отрицательные значения; Шаблон переменной enum_bounds
должен быть явно специализированным.
Другая болевая точка — это платформо-зависимая реализация enum_demangle_trim
-, которая должна сжимать базовую строку с обеих сторон для получения имен перечислений.
Несмотря на то, что сейчас 2024 год, и я использую C++20, я все еще использую X-макросы для этого, поскольку мне еще предстоит найти лучшее решение (без внешней библиотеки):
#define DEFINE_ENUM_VAL(class_name, name) name,
#define DEFINE_ENUM_STR_CONVERSION(class_name, name) case class_name::name: return #name;
#define FOR_EACH_FOOBAR(DO) \
DO(FooBar, ONE) \
DO(FooBar, TWO) \
DO(FooBar, THREE)
enum class FooBar
{
FOR_EACH_FOOBAR(DEFINE_ENUM_VAL)
};
inline auto to_string(FooBar val) -> std::string
{
switch (val) {
FOR_EACH_FOOBAR(DEFINE_ENUM_STR_CONVERSION)
}
std::unreachable();
}
#undef FOR_EACH_FOOBAR
#undef DEFINE_ENUM_STR_CONVERSION
#undef DEFINE_ENUM_VAL
Это довольно легко расширить для любого поведения, которое может быть получено из имени типа перечисления и идентификаторов значений. Тем не менее, большую часть окружающего шаблона придется повторять для каждого типа перечисления, который вы хотите поддерживать (хотя я уверен, что это можно было бы обобщить немного дальше, если вы хотите избежать этого, за счет еще большей зависимости от магии препроцессора. ).
В частности, без отражения нет способа избежать (многословного и подверженного ошибкам) повторения имен для создания строк из идентификаторов, кроме использования препроцессора.
Невозможно заставить компилятор сопоставить имена перечислений со строками. Есть несколько библиотек отражений, которые помогут вам в этом. Возможно, C++26 получит отражение, и это можно будет сделать изначально.