【问题标题】:Factory for a template class with enum template parameter具有枚举模板参数的模板类的工厂
【发布时间】:2023-04-03 19:55:01
【问题描述】:

假设我有

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

实现这个功能最优雅的方式是什么?

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

【问题讨论】:

    标签: c++ templates template-meta-programming


    【解决方案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。如果将外部 lambda 替换为函数,它应该也可以在 C++17 中使用。

    MSVC 不喜欢内部 lambda,因此如果您使用它,您可能还需要将其转换为函数。 (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)]();
    }
    

    【讨论】:

    • 哇 :) 不知道我...语法。非常感谢@HolyBlackCat!!
    • 坏消息是即使没有模板 lambdas gcc 7.3.1 也无法处理此代码。在内部 lambda 中,当我们返回 Pencil 的 unique_ptr 时,出现以下错误:“错误:参数包未使用 '...' 展开:”
    • 看来我需要 gcc 11+ (正在尝试使用gcc.godbolt.org
    • @MojoRisin 见编辑。所以这与现代 MSVC 中的问题相同,需要您也用函数替换内部 lambda。
    • 谢谢@HolyBlackCat。实际上,我需要为 gcc 7.3.1 指定 std::array 的模板参数(在 makePencilFactoryFuncs 中)。扣除模板参数失败:“错误:类模板参数扣除失败”
    【解决方案2】:

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

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

    【讨论】:

    • 是的,这是最明显的解决方案。但我不喜欢这个解决方案的几件事。 1)如果我们添加新颜色,我们需要增强它。 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]();
    }
    

    但是,没有人能完全满足您的要求:

    当我决定引入新颜色时,我想避免更改此函数(或其助手)。我觉得我们在编译时拥有所有信息来实现这一目标

    这至少有两个原因:

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

    除了尽可能避免使用模板外,没有多少解决方案可以违背第一点。 就您而言,是否有充分的理由使用模板?你真的需要使用完全不同的类吗?你不能把你的模板改成构造函数中传递的普通参数或作为类成员吗?

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

    colors.hpp:

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

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

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

    【讨论】:

    • 您也可以使用宏来代替 x-list 的单独包含。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-25
    • 1970-01-01
    相关资源
    最近更新 更多