模板类的工厂方法

时间:2018-11-12 03:58:26

标签: c++ c++11 factory-method

我在尝试建立工厂功能时遇到了一个问题, 给定一个ID和一个类型,它将返回正确的(模板化)子类。

这是要解决的问题

id()值一建立连接就通过网络发送,并向接收者指定如何对字节序列进行编码。接收者预先知道它期望的类型T,但是直到它获得该值之前,不知道该类型T如何在线路上编码。它还指定返回时如何整理返回值(某种类型的U,其中U可能与T的类型相同或不同)。通常使用此代码,即有多个使用/期望不同类型的发送者/接收者;但是,给定发送方/接收方对之间使用的类型始终是固定的。

问题的基本示意图:我们有一个(简化的)基类,它定义了id()

template <typename T>
class foo
{
public:

    virtual ~foo() { }

    // Other methods

    // This must return the same value for every type T
    virtual std::uint8_t id() const noexcept = 0;
};

从那里开始,我们有一些子类:

template <typename T>
class bar : public foo<T>
{
public:
    std::uint8_t id() const noexcept override { return 1; }
};

template <typename T>
class quux : public foo<T>
{
public:
    std::uint8_t id() const noexcept override { return 2; }
};

对于实际的工厂功能,我需要存储一些 删除类型(例如bar,quux),以便我可以存储实际 均质容器中的创建函数。 实际上,我想要的语义大致等同于:

struct creation_holder
{
    // Obviously this cannot work, as we cannot have virtual template functions
    template <typename T>
    virtual foo<T>* build() const;
};

template <template <typename> class F>
struct create : public creation_holder
{
    // As above
    template <typename T>
    foo<T>* build() const override
    {
        return new F<T>();
    }
};

std::unordered_map<std::uint8_t, create*>& mapping()
{
    static std::unordered_map<std::uint8_t, create*> m;
    return m;
}

template <typename T, template <typename> class F>
bool register_foo(F<T> foo_subclass,
    typename std::enable_if<std::is_base_of<foo<T>, F<T>>::value>::type* = 0)
{
    auto& m = mapping();
    const auto id = foo_subclass.id();
    creation_holder* hold = new create<F>();
    // insert into map if it's not already present
}

template <typename T>
foo<T>* from_id(std::uint8_t id)
{
    const auto& m = mapping();
    auto it = m.find(id);
    if(it == m.end()) { return nullptr; }
    auto c = it->second;
    return c->build<T>();
}

我尝试了很多方法来尝试获得类似的东西 语义,但没有运气。有没有办法做到这一点(我不在乎 实施方式有很大不同)。

1 个答案:

答案 0 :(得分:3)

一些用于传递类型和类型束的实用程序类型:

template<class...Ts>
struct types_t {};
template<class...Ts>
constexpr types_t<Ts...> types{}; // C++14.  In C++11, replace types<T> with types_t<T>{}.  Then again, I don't use it.
template<class T>
struct tag_t {};
template<class T>
constexpr tag_t<T> tag{}; // C++14.  In C++11, replace tag<T> with tag_t<T>{} below

现在我们写一个多工厂。

这里是ifactory

template<template<class...>class Z, class T>
struct ifactory {
  virtual std::unique_ptr<Z<T>> tagged_build(tag_t<T>) const = 0;
  virtual ~ifactory() {}
};

您传入要构建的标签,然后得到一个对象。很简单。

然后我们将它们捆绑在一起(在 1 中会更容易,但是您要求的是):

template<template<class...>class Z, class Types>
struct poly_ifactory_impl;

一种情况:

template<template<class...>class Z, class T>
struct poly_ifactory_impl<Z,types_t<T>>:
  ifactory<Z, T>
{
  using ifactory<Z, T>::tagged_build;
};

2种以上情况:

template<template<class...>class Z, class T0, class T1, class...Ts>
struct poly_ifactory_impl<Z,types_t<T0, T1, Ts...>>:
  ifactory<Z, T0>,
  poly_ifactory_impl<Z, types_t<T1, Ts...>>
{
  using ifactory<Z, T0>::tagged_build;
  using poly_ifactory_impl<Z, types_t<T1, Ts...>>::tagged_build;
};

我们将tagged_build方法导入到派生类中。这意味着派生最多的poly_ifactory_impl在相同的重载集中拥有所有tagged_build方法。我们将使用它来分派给他们。

然后我们将其包装得很漂亮:

template<template<class...>class Z, class Types>
struct poly_ifactory:
  poly_ifactory_impl<Z, Types>
{
  template<class T>
  std::unique_ptr<Z<T>> build() const {
    return this->tagged_build(tag<T>);
  }
};

注意,我要返回unique_ptr;从工厂方法中恢复原始T*就是代码异味。

拥有poly_ifactory<?>的人只会做->build<T>()而忽略tagged_重载(除非他们想要它们;我让他们暴露在外)。每个tagged_build是虚拟的,但build<T>不是虚拟的。这就是我们模拟虚拟模板函数的方式。


这处理接口。另一方面,我们不需要手动实现每个build(tag_t<T>)。我们可以使用CRTP来解决这个问题。

template<class D, class Base, template<class...>class Z, class T>
struct factory_impl : Base {
  virtual std::unique_ptr<Z<T>> tagged_build( tag_t<T> ) const override final {
    return static_cast<D const*>(this)->build_impl( tag<T> );
  }
  using Base::build;
};

template<class D, class Base, template<class...>class Z, class Types>
struct poly_factory_impl;

1种情况:

template<class D, class Base, template<class...>class Z, class T0>
struct poly_factory_impl<D, Base, Z, types_t<T0>> :
  factory_impl<D, Base, Z, T0>
{
  using factory_impl<D, Base, Z, T0>::tagged_build;
};

2+类型的情况:

template<class D, class Base, template<class...>class Z, class T0, class T1, class...Ts>
struct poly_factory_impl<D, Base, Z, types_t<T0, T1, Ts...>> :
  factory_impl<D, poly_factory_impl<D, Base, Z, types_t<T1, Ts...>>, Z, T0>
{
  using factory_impl<D, poly_factory_impl<D, Base, Z, types_t<T1, Ts...>>, Z, T0>::tagged_build;
};

这是编写tagged_build(tag_t<T>)方法的一系列ifactory重载,并将它们重定向到D::build_impl(tag_t<T>),其中D是理论派生类型。

为了避免必须使用虚拟继承,存在了花哨的“ pass Basearound”。我们线性继承,每个步骤实现一个tagged_build(tag<T>)重载。他们全都使用CRTP非虚拟地向下调度。

使用方式如下:

struct bar {};

using my_types = types_t<int, double, bar>;

template<class T>
using vec = std::vector<T>;
using my_ifactory = poly_ifactory< vec, my_types >;

struct my_factory :
  poly_factory_impl< my_factory, my_ifactory, vec, my_types >
{
  template<class T>
  std::unique_ptr< vec<T> > build_impl( tag_t<T> ) const {
    return std::make_unique< std::vector<T> >( sizeof(T) );
    // above is C++14; in C++11, use:
    // return std::unique_ptr<vec<T>>( new vec<T>(sizeof(T)) );
  }
};

并且my_factory的实例满足my_ifactory接口。

在这种情况下,我们为T的向量创建一个唯一的ptr,其元素数等于sizeof(T)。只是玩具而已。

Live example


伪代码设计。

该界面有一个

template<class T> R build

功能。派遣到

virtual R tagged_build(tag_t<T>) = 0;

方法。

有问题的Ttypes_t<Ts...>列表中提取。仅支持这些类型。

在实现方面,我们创建CRTP帮助器的线性继承。每个继承自最后一个,并覆盖virtual R tagged_build(tag_t<T>)

tagged_build的实现使用CRTP将this指针转换为派生自更高级别的类,并在其上调用build_impl(tag<T>)。这是非运行时多态的一个示例。

因此,呼叫从build<T>virtual tagged_build(tag_t<T>)build_impl(tag<T>)。用户只需与一个模板进行交互;实现者仅实现一个模板。中间的胶水virtual tagged_build是从types_t类型列表中生成的。

这是大约100行的“胶水”或帮助程序代码,作为交换,我们获得了有效的虚拟模板方法。


中的

1 变为:

template<template<class...>class Z, class...Ts>
struct poly_ifactory_impl<Z,types_t<Ts...>>:
  ifactory<Z, Ts>...
{
  using ifactory<Z, Ts>::tagged_build...;
};

这更加简单明了。


最后,您可以像这样含糊其词地执行某项操作,而无需中央类型列表。如果您知道呼叫者和被叫者都知道类型,则可以将typeidtypeindex传递到ifactory中,并通过虚拟调度派发void*或类似的东西机制,并强制转换/检查是否为空/在映射中查找类型。

内部实现看起来与此类似,但是您不必将types_t作为正式(或二进制)接口的一部分发布。

在外部,您将不得不“仅知道”支持哪些类型。在运行时,如果传入不受支持的类型,您可能会得到一个空的智能(或哑巴,ick)指针。

稍加注意,您甚至可以同时进行。公开一种有效,安全的机制,以将编译时已知的类型应用于模板。还公开一个基于“ try”的接口,该接口既使用高效的编译时已知系统(如果类型匹配),又退回到已检查的低效率运行时。您之所以这样做可能是出于深奥的向后二进制兼容性的原因(因此新软件可以通过过时的接口连接到新的或旧的API实现,并动态地处理旧的API实现)。

但是到那时,您是否考虑过使用COM?