#c #templates #template-meta-programming
Вопрос:
Предположим, у меня есть
enum class Colour
{
red,
blue,
orange
};
class PencilBase
{
public:
virtual void paint() = 0;
};
template <Colour c>
class Pencil : public PencilBase
{
void paint() override
{
// use c
}
};
Теперь я хочу иметь некоторую заводскую функцию для создания художников
PencilBase* createColourPencil(Colour c);
Какой будет самый элегантный способ реализации этой функции?
Я хочу избежать внесения изменений в эту функцию (или ее помощников), когда я решу ввести новый цвет. Я чувствую, что у нас есть вся информация во время компиляции для достижения этой цели, однако мне трудно найти решение.
Ответ №1:
Во-первых, вам нужно знать, сколько цветов существует:
enum class Colour
{
red,
blue,
orange,
_count, // <--
};
После того, как вы узнаете число, вы можете создать массив указателей на функции такого размера, каждая функция создает соответствующий класс. Затем вы используете перечисление в качестве индекса в массиве и вызываете функцию.
std::unique_ptr<PencilBase> createColourPencil(Colour c)
{
if (c < Colour{} || c >= Colour::_count)
throw std::runtime_error("Invalid color enum.");
static constexpr auto funcs = []<std::size_t ...I>(std::index_sequence<I...>)
{
return std::array{ []() -> std::unique_ptr<PencilBase>
{
return std::make_unique<Pencil<Colour(I)>>();
}...};
}(std::make_index_sequence<std::size_t(Colour::_count)>{});
return funcs[std::size_t(c)]();
}
Для шаблонов lambas требуется C 20. Если вы замените внешний лямбда-символ функцией, он должен работать и в C 17.
MSVC не нравится внутренняя лямбда, поэтому, если вы ее используете, вам может потребоваться преобразовать ее в функцию. (У GCC и Clang нет проблем с этим.)
Я использовал unique_ptr
здесь, но ничто не мешает вам использовать необработанные указатели.
gcc 7.3.1 не может обработать этот код
Вот оно, перенесено в GCC 7.3:
template <Colour I>
std::unique_ptr<PencilBase> pencilFactoryFunc()
{
return std::make_unique<Pencil<I>>();
}
template <std::size_t ...I>
constexpr auto makePencilFactoryFuncs(std::index_sequence<I...>)
{
return std::array{pencilFactoryFunc<Colour(I)>...};
}
std::unique_ptr<PencilBase> createColourPencil(Colour c)
{
if (c < Colour{} || c >= Colour::_count)
throw std::runtime_error("Invalid color enum.");
static constexpr auto funcs = makePencilFactoryFuncs(std::make_index_sequence<std::size_t(Colour::_count)>{});
return funcs[std::size_t(c)]();
}
Комментарии:
1. Вау 🙂 Не знал о синтаксисе I …. Большое спасибо @HolyBlackCat!!
2. Плохая новость заключается в том, что даже без шаблонных лямбд gcc 7.3.1 не может обработать этот код. Во внутреннем лямбда-выражении, когда мы возвращаем unique_ptr для Pencil, я получаю следующую ошибку: «ошибка: пакеты параметров не расширяются с помощью ‘…’:»
3. Похоже, для этого мне нужен gcc 11 (пытался с gcc.godbolt.org )
4. @MojoRisin См. Редактирование. Таким образом, это та же проблема, что и в современном MSVC, требующая, чтобы вы также заменили внутренний лямбда-символ функцией.
5. Спасибо @HolyBlackCat. На самом деле мне нужно было указать аргументы шаблона для std::array (в makePencilFactoryFuncs) для gcc 7.3.1. Не удалось вычесть аргументы шаблона: «ошибка: ошибка вычета аргумента шаблона класса»
Ответ №2:
Я не думаю, что есть миллионы решений, нет?
PencilBase* createColourPencil(Colour c)
{
switch (c)
{
case Colour::red:
return new Pencil<Colour::red> ();
break;
...
default:
break;
}
}
Комментарии:
1. Да, это наиболее очевидное решение. Но мне не нравятся некоторые вещи в этом решении. 1) Если мы добавим новый цвет, нам нужно его улучшить. 2) Для каждого случая мы указываем цвет дважды
2. Таким образом, решение состоит в том, чтобы не использовать шаблоны, поскольку именно во время компиляции вы ищете поведение в режиме реального времени, которое должно использовать наследование.
Ответ №3:
У вас есть несколько хороших возможностей:
PencilBase* createPencil (Colour colour) {
switch (colour) {
case RED:
return new Pencil<RED>();
case BLUE:
return new Pencil<BLUE>();
...
default:
throw std::invalid_argument("Unsupported colour");
}
}
или почему бы не использовать более современную конструкцию, подобную этой, преимущество которой заключается в потенциальном обновлении во время выполнения:
std::unordered_map<Colour, std::function<PencilBase*()>> FACTORIES = {
{ RED, [](){ new Pencil<RED>(); } },
{ BLUE, [](){ new Pencil<BLUE>(); } },
...
};
PencilBase* createPencil (Colour colour) {
return FACTORIES[colour]();
}
Однако ни один из них не может полностью выполнить то, что вы просите:
Я хочу избежать внесения изменений в эту функцию (или ее помощников), когда я решу ввести новый цвет. Я чувствую, что у нас есть вся информация во время компиляции для достижения этой цели
Для этого есть как минимум две причины:
- Ваш параметр цвета, заданный вашей фабрике, известен во время выполнения, в то время как ваш параметр шаблона должен быть известен во время компиляции.
- Нет встроенного решения для перечисления или просмотра всех значений enum
Существует не так много решений, которые противоречат первому пункту, кроме как избегать шаблонов, если вы можете. В вашем случае есть веская причина для использования шаблонов? Вам действительно нужно использовать совершенно другой класс? Разве вы не можете изменить свой шаблон на обычный параметр, передаваемый в конструкторе или как член класса?
Если у вас нет выбора использовать шаблоны, существует способ избежать повторения. Вы можете использовать старые добрые макросы и особым образом, обычно называемым X-macro. Краткий пример:
colours.hpp:
COLOUR(RED)
COLOUR(BLUE)
COLOUR(ORANGE)
Color.hpp:
enum Colour {
#define COLOUR(C) C,
#include "colours.hpp"
#undef COLOUR
INVALID_COLOUR
};
Заводская функция:
PencilBase* createPencil (Colour colour) {
switch(colour) {
#define COLOUR(C) case C: return new Pencil<C>();
#include "colours.hpp"
#undef COLOUR
default:
throw new std::invalid_argument("Invalid colour");
}
}
Это в основном перепишет переключатель, приведенный в качестве первого примера. Вы также можете изменить макрос, чтобы переписать сопоставление. Однако, как вы можете видеть, он может быть не более удобочитаемым или обслуживаемым, чем быть явным.
Комментарии:
1. Вы также можете использовать макрос вместо отдельного включения для x-списка.