模板的工厂方法 类
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() {}
};
你传入你想要构建的标签,然后你得到一个对象。很简单。
然后我们将它们捆绑起来(这在 c++171, but you asked for c++11 中会更容易):
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)
。这只是一个玩具。
伪代码设计
界面有一个
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 在 c++17 这变成:
template<template<class...>class Z, class...Ts>
struct poly_ifactory_impl<Z,types_t<Ts...>>:
ifactory<Z, Ts>...
{
using ifactory<Z, Ts>::tagged_build...;
};
这样更简单明了。
最后,您可以像这样模糊地做一些事情,而无需中央类型列表。如果您知道调用者和被调用者都知道类型,您可以将 typeid
或 typeindex
传递到 ifactory
,通过虚拟调度传递 void*
或类似的东西机制,cast/check null/do 在类型映射中查找。
内部实现看起来与此类似,但您不必将 types_t
作为正式(或二进制)接口的一部分发布。
在外部,您必须 "just know" 支持哪些类型。在运行时,如果你传入一个不受支持的类型,你可能会得到一个空的智能(或愚蠢,恶心)指针。
稍加小心,您甚至可以同时做到这两点。公开一种高效、安全的机制来获取应用于模板的编译时已知类型。还公开了一个基于 "try" 的接口,该接口既使用高效的编译时已知系统(如果类型匹配)又退回到已检查的低效运行时。您可能出于深奥的向后二进制兼容性原因而这样做(因此新软件可以通过过时的接口连接到新的或旧的 API 实现并动态处理旧的 API 实现)。
但是那个时候,你考虑过使用 COM 吗?
我在尝试构建工厂功能时遇到了一个问题, 给定一个 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() {}
};
你传入你想要构建的标签,然后你得到一个对象。很简单。
然后我们将它们捆绑起来(这在 c++171, but you asked for c++11 中会更容易):
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)
。这只是一个玩具。
伪代码设计
界面有一个
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 在 c++17 这变成:
template<template<class...>class Z, class...Ts>
struct poly_ifactory_impl<Z,types_t<Ts...>>:
ifactory<Z, Ts>...
{
using ifactory<Z, Ts>::tagged_build...;
};
这样更简单明了。
最后,您可以像这样模糊地做一些事情,而无需中央类型列表。如果您知道调用者和被调用者都知道类型,您可以将 typeid
或 typeindex
传递到 ifactory
,通过虚拟调度传递 void*
或类似的东西机制,cast/check null/do 在类型映射中查找。
内部实现看起来与此类似,但您不必将 types_t
作为正式(或二进制)接口的一部分发布。
在外部,您必须 "just know" 支持哪些类型。在运行时,如果你传入一个不受支持的类型,你可能会得到一个空的智能(或愚蠢,恶心)指针。
稍加小心,您甚至可以同时做到这两点。公开一种高效、安全的机制来获取应用于模板的编译时已知类型。还公开了一个基于 "try" 的接口,该接口既使用高效的编译时已知系统(如果类型匹配)又退回到已检查的低效运行时。您可能出于深奥的向后二进制兼容性原因而这样做(因此新软件可以通过过时的接口连接到新的或旧的 API 实现并动态处理旧的 API 实现)。
但是那个时候,你考虑过使用 COM 吗?