具有枚举模板参数的模板 class 的工厂

Factory for a template class with enum template parameter

假设我有

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);

实现此功能最优雅的方法是什么?

当我决定引入一种新颜色时,我想避免更改此函数(或其助手)。 我觉得我们在编译时拥有实现此目的的所有信息,但是我很难找到解决方案。

我不认为有数百万的解决方案不是吗?

PencilBase* createColourPencil(Colour c)
{
    switch (c)
    {
        case Colour::red:
            return new Pencil<Colour::red> ();
        break;
        ...
        
        default:
        break;
    }
}

首先你要知道有多少种颜色:

enum class Colour
{
    red,
    blue,
    orange,
    _count, // <--
};

知道数字后,您可以创建一个此大小的函数指针数组,每个函数创建各自的 class。然后使用枚举作为数组的索引,并调用函数。

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。如果用函数替换外部 lambda,它应该也适用于 C++17。

MSVC 不喜欢内部 lambda,因此如果您正在使用它,您可能还需要将其转换为函数。 (GCC 和 Clang 都没有问题。)

我在这里使用了 unique_ptr,但没有什么能阻止您使用原始指针。


gcc 7.3.1 is not able to handle this code

在这里,移植到 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)]();
}

你有几个很好的可能性:

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]();
}

不过,none完全可以满足你的要求:

I want to avoid making changes in this function (or its helpers) when I decide to introduce a new colour. I feel like we have all the information at compile time to achieve this

至少有两个原因:

  • 您提供给工厂的颜色参数在运行时已知,而您的模板参数必须在编译时已知。
  • 没有内置解决方案来枚举或遍历所有枚举值

与第一点相反的解决方案并不多,除了尽可能避免使用模板。 在您的情况下,是否有充分的理由使用模板?您真的需要使用完全不同的 class 吗?不能将模板更改为在构造函数中传递的普通参数或作为 class 成员吗?

如果您别无选择使用模板,有一种方法可以避免重复您自己。您可以使用很好的旧宏,并以一种通常称为 X-macro 的特殊方式使用。快速示例:

colours.hpp:

COLOUR(RED)
COLOUR(BLUE)
COLOUR(ORANGE)

Colour.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");
  }
}

这将基本上重写作为第一个示例给出的开关。您也可以更改宏以重写映射。但是,如您所见,它可能并不比显式更易读或更易于维护。