Фабрика для шаблонного класса с параметром шаблона enum

#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-списка.