В настоящее время все знают дрель. Когда вы можете перечислить все возможные значения типа, хорошей идеей будет превратить его в перечисление.
enum Fruit { Apple, Orange, Peach };
Просто и чисто. Таким образом, ваша IDE может дать вам аккуратный список автозаполнения, компилятор может предупредить вас, если вы пропустите значение в переключателе, и все будут довольны. Конец истории, да?
К сожалению нет.
На практике первое, что вы заметите, это то, что все значения перечисления принадлежат родительскому пространству имен и смешиваются.
enum Fruit { Apple, Orange, Peach }; enum Color { Red, Green, Blue }; int main() { Fruit favorite_fruit = Apple; // not Fruit::Apple Color favorite_color = Red; // not Color::Red }
Это не очень хорошая идея на многих разных уровнях. При объявлении значения типа Fruit использование Apple вместо Fruit::Apple заставляет меня думать, что поскольку Red находится в том же пространстве имен, оно также может быть допустимым значением Fruit, хотя на самом деле это Color. Это также дает мне плохое представление о том, каковы значения Fruit в целом, и довольно часто мне приходится вставать со стула и искать объявление Fruit, чтобы быть полностью уверенным.
Кроме того, эта практика загромождает пространство имен множеством глобальных значений, что затрудняет навигацию и различие между объектами.
И, наконец, вы не можете одновременно иметь фрукт с именем Оранжевый и цвет с именем Оранжевый в одном пространстве имен.
В C++11 эти проблемы решаются с помощью новых правил доступа к перечислению и оператора enum class. Вот как это работает.
enum class Fruit { Apple, Orange, Peach }; enum class Color { Red, Green, Blue }; int main() { Fruit favorite_fruit = Fruit::Apple; Color favorite_color = Color::Red; }
Счастлив теперь? Еще не совсем.
Что, если бы я написал простую библиотеку вокруг двух моих перечислений и какое-то время использовал ее в программах, связанных с фруктами. Затем, после хорошего отпуска на Карибах, я решил создать libtropical, что бы добавить еще немного фруктов?
То, как мои фруктовые перечисления объявлены сейчас, у меня не было бы возможности расширить перечисления, кроме как перейти к исходной библиотеке и добавить новые значения в объявление перечисления, что, вероятно, сломает мой код в нескольких местах в процессе.
Это больше не является хорошим вариантом использования для перечислений. Полиморфизм во время выполнения спешит на помощь, я прав?
namespace fruits { struct FruitBase {}; struct Apple: FruitBase {}; struct Orange: FruitBase {}; struct Peach: FruitBase {}; } // Later, in libtropical: namespace fruits { struct Pineapple: FruitBase {}; struct Coconut: FruitBase {}; }
Это снова выглядит красиво и чисто. А позже любая библиотека может пойти дальше и объявить некоторые расширения FruitBase в своем пространстве имен. Но как вы используете эту концепцию на практике?
Давайте теперь оставим счастливый мир фруктов и цветов и представим конкретный вариант использования, когда вы пытаетесь связаться с каким-то другим процессом, используя общую память, чтобы прочитать некоторые параметры.
У вас изначально был метод, который работал с перечислением. Ничего красивого, но это работало довольно хорошо, учитывая высокую скорость входящих сообщений с данными параметров.
enum Param { First, Second, Third }; struct FirstParamData { /* ... */ }; struct SecondParamData { /* ... */ }; struct ThirdParamData { /* ... */ }; FirstParamData data1; SecondParamData data2; ThirdParamData data3; // This method gets called when data arrives. // `input_data` must point to 32 bytes of memory void read_param(Param param, const void* input_data) { switch (param) { case Param::First: data1 = *(FirstParamData*) input_data; break; case Param::Second: data2 = *(SecondParamData*) input_data; break; case Param::Third: data3 = *(ThirdParamData*) input_data; break; } }
Давайте теперь посмотрим, что происходит, когда мы пытаемся дать ему ту же расширяемую полиморфную обработку, которую мы описали выше.
Мы перемещаем все из метода чтения в объявление структур param, заканчивая кучей структур и виртуальных методов.
struct ParamBase { virtual void read_data(const void* input_data) const = 0; }; struct First: ParamBase { virtual void read_data(const void* input_data) const override { data1 = *(FirstParamData*) input_data; } }; struct Second: ParamBase { virtual void read_data(const void* input_data) const override { data2 = *(SecondParamData*) input_data; } }; // This method gets called when data arrives. // `input_data` must point to 32 bytes of memory void read_param(std::shared_ptr<ParamBase> param, const void* input_data) { param->read_data(input_data); }
Это не плохо, но и не хорошо.
Во-первых, обратите внимание, что код для структур параметров выглядит почти одинаково в обоих случаях. Много повторяться при написании кода — нехороший знак.
Кроме того, виртуальные методы довольно медленны в высокопроизводительных приложениях. И кому-то придется выделять и освобождать эти общие указатели каждый раз, когда приходит пакет данных. Более того, поскольку они не несут полезной информации, кроме дискриминатора параметров, это не очень хорошая сделка.
Есть ли лучшее решение? Я думаю, что, возможно, нашел один.
Мы можем использовать оператор typeid() вместе с некоторыми шаблонами, чтобы полностью обойти полиморфизм времени выполнения и получить что-то столь же расширяемое и аккуратное, как примеры фруктов, без хлопот постоянного перераспределения памяти и вызовов виртуальных методов. .
Для тех, кто, как и я, изначально понятия не имел, что делает typeid(): он сопоставляет типы и переменные с std::type_info во время компиляции, давая вам возможность извлекать введите имя или сравните типы различных переменных во время выполнения. Однако есть небольшая загвоздка — std::type_info не является CopyConstructible, поэтому вы не можете слишком много перемещать его. Для этого вы можете использовать std::type_index, который сохраняет все свои свойства, будучи CopyConstructible. Подробнее здесь.
Давайте начнем со структур параметров в пространстве имен. Кроме того, пусть каждый параметр объявляет свой тип структуры данных.
struct FirstParamData { /* ... */ }; struct SecondParamData { /* ... */ }; struct ThirdParamData { /* ... */ }; namespace params { struct ParamBase {}; struct First: ParamBase { using data_type = FirstParamData; }; struct Second: ParamBase { using data_type = SecondParamData; }; struct Third: ParamBase { using data_type = ThirdParamData; }; }
Затем создайте шаблонный вспомогательный метод, который копирует заданную память в структуру, зависящую от параметра, но только в том случае, если утверждение типа верно.
// `data` must point to 32 bytes of memory template<typename ParamType, typename TargetType = typename ParamType::data_type> void read_param_helper(std::type_index param, TargetType& target, const void* data) { if (param != typeid(ParamType)) return; target = *(TargetType*) data; }
Тогда его использование в исходном методе чтения довольно простое и элегантное.
FirstParamData data1; SecondParamData data2; ThirdParamData data3; // This method gets called when data arrives. // `data` must point to 32 bytes of memory void read_param_data(std::type_index param, const void* input) { read_param_helper<param::First>(param, data1, input); read_param_helper<param::Second>(param, data2, input); read_param_helper<param::Third>(param, data3, input); }
Этот метод будет немного сложнее исходного, основанного на перечислении, из-за всех вызовов вспомогательных методов (в оригинале вызывалась только одна ветвь switch). Однако, если вы используете современный компилятор, это не имеет значения, так как он будет оптимизирован до -O3. И не будет уродливого объема памяти или виртуальных вызовов, как в предыдущей попытке.
Если появится новая версия с большим количеством параметров, мы можем безопасно расширить пространство имен param из другой библиотеки, связать его с исходной библиотекой, и ничего не сломается.
Кроме того, мы получаем дополнительное преимущество проверки типов во время компиляции. Это то, чего мы не получили бы в исходной системе, основанной на перечислении, или в первой неудачной попытке полиморфного решения.
// Compiles: FirstParamData data1; read_param_helper<param::First>(param, data1, input); // Does not compile – converting FirstParamData to SecondParamData: SecondParamData data2; read_param_helper<param::First>(param, data2, input);
И, наконец, мы можем полностью избежать имен структур, таких как FirstParamData, используя оператор using в объявлениях.
param::First::data_type data1;
Таким образом, если позже мы решим заменить FirstParamData какой-либо другой структурой, мы сможем сделать это в одном месте, а остальная часть кода адаптируется автоматически.