模板的工厂方法 类

Factory method for template classes

我在尝试构建工厂功能时遇到了一个问题, 给定一个 ID 和一个类型将 return 正确的(模板化的)subclass.

这是要解决的问题:

id() 值在连接建立后立即通过网络发送,并向接收方指定字节序列的编码方式。接收方事先知道它期望的类型 T,但在它获得该值之前不知道该类型 T 在线路上是如何编码的。它还指定了 return 值(某种类型 U,其中 U 可能与 T 的类型相同或不同)在 returned 时应如何编组。这个代码是通用的,即有多个senders/receivers,use/expect不同的类型;但是,给定 sender/receiver 对之间使用的类型始终是固定的。

问题的基本概述:我们有一个(简化的)基 class 定义 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;
};

从那里,我们有一些子classes:

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

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

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

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

现在我们写一个poly ifactory。

这是一个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, but you asked for 中会更容易):

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 方法向下导入派生的 classes。这意味着最派生的 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;

第一种情况:

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

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

花哨的 "pass Base around" 的存在是为了避免必须使用虚拟继承。我们线性继承,每一步实现一个 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;

方法。

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

在实现方面,我们创建了 CRTP 助手的线性继承。每个都继承自上一个,并覆盖 virtual R tagged_build(tag_t<T>).

tagged_build 的实现使用 CRTP 将 this 指针转换为更派生的 class 并对其调用 build_impl(tag<T>)。这是非运行时多态性的一个例子。

所以调用从 build<T>virtual tagged_build(tag_t<T>) 再到 build_impl(tag<T>)。用户只需与一个模板交互;实施者只实施一个模板。中间的胶水——virtual tagged_build——是从 types_t 类型列表生成的。

这大约是 100 行 "glue" 或辅助代码,作为交换,我们得到了有效的虚拟模板方法。


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* 或类似的东西机制,cast/check null/do 在类型映射中查找。

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

在外部,您必须 "just know" 支持哪些类型。在运行时,如果你传入一个不受支持的类型,你可能会得到一个空的智能(或愚蠢,恶心)指针。

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

但是那个时候,你考虑过使用 COM 吗?