poly
目录
简介
静态多态 (static polymorphism) 是 C++ 中一个非常强大的工具,尽管有时使用起来较为繁琐。
本模块旨在使其变得简单且易于使用。
该库允许将 concept 定义为接口,由具体类来满足,而无需从公共基类继承。
除其他优点外,这是静态多态的一般优势,也是 poly 类模板提供的通用包装器 (generic wrapper) 的特定优势之一。
其结果是一个可以作为对象本身传递的对象,而不是通过引用或指针传递,这与使用动态多态时的情况不同。
由于 poly 类模板在内部使用 entt::any,它也支持其大部分功能。例如,为现有且未托管的对象创建别名 (aliases) 的可能性。这允许用户在保持对象所有权的同时利用静态多态。
同样,poly 类模板也受益于 entt::any 类提供的小缓冲区优化 (small buffer optimization),因此可以最小化内存分配次数,在可能的情况下完全避免分配。
其他库
有一些关于静态多态的非常有趣的库。
我更喜欢的是:
前者坦白说是一个实验性库,具有许多有趣的想法。我对某些功能在实际项目中的实用性有一些疑问,但也许是我缺乏经验。在我看来,它唯一的缺点是 API,我觉得它比其他解决方案稍微繁琐一些。
后者无疑是本模块的灵感来源。尽管我在最终 API 和某些功能的实现上选择了不同的方案。
无论如何,这些作者都是 C++ 社区的大师,我只能向他们学习。
Concept 与实现
要创建 类型擦除的多态对象包装器(使用 Eric Niebler 引入的术语),首先要做的是定义一个 concept,类型必须遵循该 concept。
为此,该库提供了一个支持推导接口和完全定义接口的单一类。尽管自动推导接口很方便,并允许用户在大多数情况下编写更少的代码,但它有一些局限性。因此,能够通过提供静态虚函数表 (static virtual table) 的自定义定义来绕过推导是很有用的。
一旦定义了接口,就需要一个通用实现来满足该 concept 本身。
同样在这种情况下,该库允许基于类型或类型族 (families of types) 进行定制,以便在必要时能够超越通用情况。
推导接口
这是定义具有推导接口的 concept 的方式:
struct Drawable: entt::type_list<> {
template<typename Base>
struct type: Base {
void draw() { this->template invoke<0>(*this); }
};
// ...
};
它可以通过继承空类型列表 (empty type list) 来识别。
函数也可以是 const、接受任意数量的参数并返回 void 以外的类型:
struct Drawable: entt::type_list<> {
template<typename Base>
struct type: Base {
bool draw(int pt) const { return this->template invoke<0>(*this, pt); }
};
// ...
};
在这种情况下,所有参数都在对 this 的引用之后传递给 invoke,返回值是内部调用返回的任何内容。
至于 invoke,这是一个通过 Base 注入到 concept 中的名称,必须从其继承。由于它也是一个依赖名称 (dependent name),由于语言的规则,不幸的是需要 this-> template 形式。然而,也存在一种通过外部调用的替代方案:
struct Drawable: entt::type_list<> {
template<typename Base>
struct type: Base {
void draw() const { entt::poly_call<0>(*this); }
};
// ...
};
一旦定义了 concept,用户必须提供其通用实现,以告诉系统任何类型如何满足其要求。这是通过 concept 本身内的别名模板完成的。
传递给 invoke 或 poly_call 的模板参数索引引用了此别名的定义方式。
定义接口
完全定义的 concept 与接口被推导的 concept 没有区别,唯一的区别是这次类型列表不为空:
struct Drawable: entt::type_list<void()> {
template<typename Base>
struct type: Base {
void draw() { entt::poly_call<0>(*this); }
};
// ...
};
同样,允许 void 以外的参数和返回值。此外,当要绑定的方法是 const 时,函数类型也必须是 const:
struct Drawable: entt::type_list<bool(int) const> {
template<typename Base>
struct type: Base {
bool draw(int pt) const { return entt::poly_call<0>(*this, pt); }
};
// ...
};
如果函数类型与推导的类型相同,用户为什么要完全定义 concept?
事实上,这是可以通过手动定义静态虚函数表来绕过的限制。
当内容被推导时,存在一个隐式约束。
如果 concept 公开了一个名为 draw、函数类型为 void() 的成员函数,则满足 concept:
- 要么通过公开具有相同名称和相同签名的成员函数的类。
- 要么通过使用接口本身现有成员函数的 lambda。
换句话说,不可能使用不属于接口的函数,即使它们是满足 concept 的类型的一部分。
同样,不可能在静态虚函数表中推导一个函数类型与接口中关联成员函数不同的函数。
显式定义静态虚函数表会抑制推导步骤,并在为 concept 提供实现时允许最大的灵活性。
满足 Concept
concept 的 impl 别名模板用于定义如何满足它:
struct Drawable: entt::type_list<> {
// ...
template<typename Type>
using impl = entt::value_list<&Type::draw>;
};
在这种情况下,声明通用类型的 draw 方法足以满足 Drawable concept 的要求。
支持成员函数和自由函数来满足 concept:
template<typename Type>
void print(Type &self) { self.print(); }
struct Drawable: entt::type_list<void()> {
// ...
template<typename Type>
using impl = entt::value_list<&print<Type>>;
};
同样,只要参数类型和返回类型支持与静态虚函数表中引用的函数类型之间的转换,实际实现在其函数类型上可能有所不同,因为它在内部被擦除。
此外,self 参数不是系统严格要求的,如果不需要,可以为自由函数省略它。
有关更多详细信息,请参阅内联文档。
继承
由于 poly 在 EnTT 中的外观,concept 继承 非常简单。因此,如果需要,构建 concept 层次结构相当容易。
唯一的约束是层次结构中的所有 concept 必须属于同一个 族 (family),即它们必须全部是推导的或全部是定义的。
对于推导的 concept,继承通过几个步骤实现:
struct DrawableAndErasable: entt::type_list<> {
template<typename Base>
struct type: Drawable::type<Base> {
static constexpr auto base = Drawable::impl<Drawable::type<entt::poly_inspector>>::size;
void erase() { entt::poly_call<base + 0>(*this); }
};
template<typename Type>
using impl = entt::value_list_cat_t<
Drawable::impl<Type>,
entt::value_list<&Type::erase>
>;
};
静态虚函数表为空且必须保持为空。
另一方面,type 不再继承自 Base。相反,它将模板参数转发给 基类 公开的类型。在内部,基类静态虚函数表的 大小 用作本地索引的偏移量。
最后,通过 value_list_cat_t 实用工具,实现包括将新函数附加到先前列表。
至于定义的 concept,类型列表以与上述 concept 实现所示类似的方式 扩展。
为此,声明一个允许将 concept 转换为其底层 type_list 对象的函数很有用:
template<typename... Type>
entt::type_list<Type...> as_type_list(const entt::type_list<Type...> &);
定义并非严格要求,因为该函数仅通过 decltype 使用,如下所示:
struct DrawableAndErasable: entt::type_list_cat_t<
decltype(as_type_list(std::declval<Drawable>())),
entt::type_list<void()>> {
// ...
};
与上面类似,type_list_cat_t 用于将底层静态虚函数表与新函数类型连接起来。
其他所有内容则与已展示的内容相同。
实际应用中的静态多态
一旦定义了 concept 和实现,就可以使用 poly 类模板来 包装 满足要求的实例:
using drawable = entt::poly<Drawable>;
struct circle {
void draw() { /* ... */ }
};
struct square {
void draw() { /* ... */ }
};
// ...
drawable instance{circle{}};
instance->draw();
instance = square{};
instance->draw();
此类提供广泛的构造函数,从默认构造函数(返回未初始化的 poly 对象)到拷贝和移动构造函数,以及就地创建对象的能力。
除其他外,还有一个构造函数允许用户将未托管的对象(无论是 const 还是非 const)包装到 poly 实例中:
circle shape;
drawable instance{std::in_place_type<circle &>, shape};
同样,可以从现有对象创建 poly 的非拥有拷贝 (non-owning copies):
drawable other = instance.as_ref();
在这两种情况下,尽管 poly 对象的接口不变,但它不会构造任何元素或负责销毁引用的对象。
还要注意,底层 concept 是通过调用 operator-> 访问的,而不是直接作为 instance.draw() 访问。
这允许用户将包装器的 API 与 concept 的 API 解耦。因此,instance.data() 调用 poly 对象的 data 成员函数,而 instance->data() 直接映射到底层 concept 公开的功能。
存储大小与对齐要求
在底层,poly 类模板使用 entt::any。因此,它可以利用在编译时定义适合小缓冲区优化的存储大小以及对齐要求的可能性:
entt::basic_poly<Drawable, sizeof(double[4]), alignof(double[4])>
默认大小为 sizeof(double[2]),这似乎是太大缓冲区与无法容纳大于整数的缓冲区之间的良好折衷。对齐要求是可选的,默认情况下,对于大小不超过所提供大小的任何对象,采用最严格(最大)的对齐要求。
值得注意的是,提供大小为 0(在所有方面都是可接受的值)将强制系统在所有情况下动态分配包含的对象。