EnTT: 静态多态 Static Polymorphism
整理 EnTT poly 静态多态系统中的概念定义、接口推导、显式接口、概念继承和对象存储。
静态多态 通过 模板 和 类型擦除 在编译时绑定行为, 跳过了 动态分派, 避免了虚函数开销, 静态多态性是 C++ 中非常强大的工具, 尽管有时使用起来很麻烦, entt 支持定义 概念 concept 作为 接口 来实现具体类, 而无需继承自公共基类, 结果是对象的直接传递, 而非像传统多态那样通过指针或引用
entt::poly 和 folly::Poly 这类库都是利用 类型擦除 配合模板元编程来实现静态多态性的一种手段, 它们的核心思想在于: 在编译期生成一个类似 传统虚表 vtable 的 静态数据结构, 但这个虚表并非由编译器隐式生成, 而是通过模板生成的 常量结构体, 从而在 运行时 能够以极低的开销实现接口调用
静态虚表 Static VTable
与传统 C++ 中通过虚函数生成的动态虚表不同, 静态多态库使用模板元编程在编译期生成一个 虚表 结构, 这个结构是一个常量数组或结构体, 其中每个元素都是指向相应接口实现函数的 指针
-
静态分派
调用接口函数时,poly对象内部会保存一个指向该类型对应 静态虚表 的 指针, 每次调用时, 都通过这个 静态表 来查找正确的 函数指针, 并传入存储在 类型擦除包装器 内的 具体对象指针, 从而调用实际的实现 -
内联优化
由于虚表在编译期已确定, 编译器有机会将这些函数调用内联, 从而减少间接调用的开销
多态包装器 (poly 对象) 内部通常会同时保存:
-
一个指向 静态虚表 的指针 (或引用), 表明当前封装对象所属的具体类型对应的调用函数
-
对象本身的存储 (有时借助类似
entt::any的机制), 这部分可能支持 小缓冲优化, 即在对象较小时避免动态内存分配
这种设计可以在不使用继承和传统虚函数机制的前提下, 依然能够传递和操作一组不同类型的对象, 同时保持接口统一性
EnTT
EnTT 在底层使用 entt::any 进行 类型擦除, 允许对象作为值传递, 同时实现多态行为, 这种方式不需要依赖虚函数表, 也不需要指针或引用, 因为多态性是在 编译时 确定的, 正因为 poly 类模版在底层使用了 entt:any, 所以支持它所支持的大部分功能, 例如, 可以为现有且不受管理的对象创建别名, 这允许用户在保持对象所有权的同时利用静态多态性
同样 poly 类模版也受益于 entt::any 提供的 小缓冲优化 small buffer optimization, 因此可以最大限度地减少分配次数, 并尽可能避免分配
定义概念
在 静态多态 中创建接口分两步
-
定义接口 Concept
用户需要为多态接口定义一个 概念, 明确规定对象需要提供哪些操作 (例如成员函数或自由函数), 例如对于一个绘制接口, 可以定义一个draw方法, 这种接口定义既可以是 自动推导 方式, 也可以 手动定义 具体的函数类型在
entt::poly的文档中,可以看到通过继承一个空的type_list或者指定函数签名来定义接口,从而让用户自定义的类型满足这个接口要求 -
实现绑定 Fulfill a Concept
对于每一个满足该接口的具体类型, 库会通过模板参数生成一个绑定 (或称 模型), 将类型中对应的方法 (或自由函数) 和接口中的函数对应起来, 这一步实现了 将具体实现绑定到抽象接口 的过程
要实际创建 类型擦除多态对象包装器 type-erasing polymorphic object wrapper, 首先要做的是定义类型必须遵循的 概念 concept, 概念 则包含一系列需要实现的 接口, 库提供了两种方式
- 自动推导接口: 方便快捷, 减少样板代码
- 显式定义接口: 灵活性更强, 适用于复杂场景
在概念定义完成后, 开发者可以根据需要对类型或类型家族进行定制化实现, 从而满足特定需求
推导接口 Deduce Interface
在静态多态中, 推导接口 是一种定义 概念 的方式, 允许根据模板类型自动推导类型的实现
在 entt 中, 推导接口定义 概念 的方式如下
struct Drawable : entt::type_list<> {
template<typename Base>
struct type : Base {
void draw() { this->template invoke<0>(*this); }
};
};entt::type_list<> 是一个空类型列表, 表明这是一个推导接口, type 是模板类, 它从 Base 继承, Base 提供了静态多态所需的支持功能 (例如 invoke), draw 是接口中的函数, 通过 this->template invoke 调用其 实现
推导接口支持更复杂的接口定义, 例如 const 修饰符, 参数与返回值
struct Drawable : entt::type_list<> {
template<typename Base>
struct type : Base {
bool draw(int pt) const { return this->template invoke<0>(*this, pt); }
};
};draw 函数接收一个 int 参数, 返回一个 bool 值, 而 invoke<0> 的第一个模板参数 0 是索引, 标记 draw 在概念中的位置, 所有参数都会传递给内部的实际实现
invoke 与 poly_call
如果是第一次见, 这种语法看起来特别奇怪, 因为 this->template invoke 是 C++ 中的一种特殊语法, 用于访问模板基类中的 依赖名称 dependent name
依赖名称是指, 在模板类中, 基类的某些成员函数或类型可能是模板参数的 依赖项, 这些成员函数或类型在编译时无法直接解析, 必须显式告诉编译器它是一个模板, 例如, 以下代码中, Base 是模板参数, 它可能包含一个成员函数 invoke
template<typename Base>
struct Derived : Base {
void call() {
this->invoke(); // 错误:编译器不知道 invoke 是一个模板函数
}
};由于 invoke 来自模板参数 Base, 编译器在解析 this->invoke() 时不知道 invoke 是:
- 一个普通成员变量
- 还是一个成员函数
- 或是一个模板函数
这就是 依赖名称 问题
为了明确告诉编译器 invoke 是一个模板函数, 需要使用关键字 template
this->template invoke<0>(*this);this 表示当前类实例, template 明确告诉编译器, invoke 是一个模板函数, <0> 是模板参数, *this 是传递给 invoke 的参数
在 C++ 模板中, 基类中的 依赖名称 (例如 invoke) 默认被认为是非模板的, 除非明确指明
template<typename Base>
struct Derived : Base {
void call() {
// 错误:编译器不知道 Base 中的 `invoke` 是模板函数
this->invoke<0>(*this);
// 正确:显式告诉编译器 `invoke` 是模板函数
this->template invoke<0>(*this);
}
};在 entt::poly 的推导接口中, invoke 是通过模板基类 Base 提供的模板函数
struct Drawable : entt::type_list<> {
template<typename Base>
struct type : Base {
void draw() {
this->template invoke<0>(*this); // 调用索引为 0 的方法
}
};
};Base提供invoke:Base是模板参数, 它为派生类提供了invoke, 用于调用实现的具体函数索引 0: 通过模板参数索引 (例如<0>) 标记调用的接口函数 (例如draw)- 参数传递:
*this是当前对象的引用, 作为实际实现函数的第一个参数
使用 entt::poly_call 替代
entt 提供了一个全局函数 entt::poly_call, 避免直接使用 this->template invoke 的复杂语法
void draw() const {
entt::poly_call<0>(*this); // 更简洁的写法
}entt::poly_call 本质上封装了对 invoke 的调用, 使代码更易读
显式定义接口 Defined Interface
在 entt::poly 中, 显式定义接口 和 推导接口 的结构和目的非常相似, 唯一区别在于 显式定义接口 通过 entt::type_list<> 提供了一个非空类型列表, 明确列出接口中所有函数的签名
显式定义接口通过 entt::type_list<> 列出所有接口函数的签名
无参数和返回值的接口
struct Drawable : entt::type_list<void()> { // 显式定义 draw 函数的类型
template<typename Base>
struct type : Base {
void draw() { entt::poly_call<0>(*this); } // 调用索引 0 的函数
};
// 其他代码...
};entt::type_list<void()> 明确指定了 draw 函数的签名: 无参数, 无返回值, 通过 entt::poly_call<0>, 与该签名绑定的函数将被调用
带参数和返回值的接口
struct Drawable : entt::type_list<bool(int) const> { // 显式定义 draw 函数的类型
template<typename Base>
struct type : Base {
bool draw(int pt) const { return entt::poly_call<0>(*this, pt); }
};
// 其他代码...
};entt::type_list<bool(int) const> 指定了 draw 函数的签名
- 参数为
int - 返回值为
bool - 函数是
const, 表示它不能修改对象状态
调用时, 通过 entt::poly_call<0> 传递参数 pt 并返回结果
显式定义接口的意义在于克服 推导接口 的一些局限性
局限 1:推导的接口有隐式约束
当使用推导接口时, entt::poly 会隐式地推断接口函数的实现, 如果接口中定义了 draw, 推导要求类型本身必须有一个同名、同签名的成员函数, 或者通过 lambda 使用已有的成员函数, 不能调用类型中存在但未在接口中声明的函数, 即使它们可以满足 概念
struct Drawable : entt::type_list<> {
template<typename Base>
struct type : Base {
void draw() { entt::poly_call<0>(*this); } // 需要类型中有 draw()
};
};
// 类没有 draw 函数,推导失败
struct MyType {
void render(); // 名字不同
};
// 无法推导 MyType::render() 与 Drawable::draw() 的关系显式定义接口可以绕过推导限制, 通过手动定义 静态虚表, 将任意函数绑定到接口
struct Drawable : entt::type_list<void()> {
template<typename Base>
struct type : Base {
void draw() { entt::poly_call<0>(*this); }
};
};
// 通过静态虚表绑定实现
template<>
struct entt::poly_vtable<Drawable> {
static constexpr auto vtable = entt::make_vtable([](MyType &self) {
self.render(); // 显式绑定 render 到 draw
});
};局限 2:推导的函数类型必须匹配接口
推导接口要求实际函数的签名与接口中定义的完全一致, 包括参数、返回值和 const 修饰符
struct Drawable : entt::type_list<> {
template<typename Base>
struct type : Base {
void draw() { entt::poly_call<0>(*this); } // 要求完全匹配 void()
};
};
// 类型中有一个类似的函数,但返回值不同
struct MyType {
int draw(); // 返回 int 而非 void
};
// 无法推导:函数签名不一致通过显式定义接口允许手动定义虚表, 绑定不同签名的函数
struct Drawable : entt::type_list<void()> {
template<typename Base>
struct type : Base {
void draw() { entt::poly_call<0>(*this); }
};
};
// 通过静态虚表绑定 MyType::draw()(返回值不同)
template<>
struct entt::poly_vtable<Drawable> {
static constexpr auto vtable = entt::make_vtable([](MyType &self) {
self.draw(); // 即使返回值不匹配,仍然可以绑定
});
};可以看到, 显示定义有以下优势
- 更灵活的绑定机制: 通过静态虚表, 可以绑定任意函数到接口, 不需要完全匹配函数签名或名称
- 控制推导过程: 显式定义接口抑制了推导步骤, 让开发者手动决定接口函数的实现
- 更复杂的接口映射: 显式定义支持将多个内部函数组合成一个接口函数, 或将接口函数拆分为多个内部函数
无法绑定 的意思是无法让某个类型实现 接口, 也就是无法满足接口所要求的 概念, 因此该类型不能被用作该接口的实现
实现概念
在 entt::poly 的实现中, 绑定的过程是通过以下两部分完成的
- 概念 Concept: 定义了接口的行为 (例如
void draw()) - 实现规则 Implementation: 通过
impl或其他方式将接口与具体类型的实现函数绑定
绑定在实例化 entt::poly 对象时自动发生, 也就是说, 当我们将一个满足概念要求的类型 (例如 circle 或 square) 包装为 entt::poly 时, entt 自动完成了接口函数到实现函数的绑定
在默认情况下, entt::poly 使用 推导接口 Deduced Interface 机制来自动完成绑定, 如果概念定义中没有显式指定 impl, 则 entt::poly 会尝试自动推导实现规则, 推导的依据是, 类型中是否有与接口函数同名、同签名的函数
struct Drawable : entt::type_list<> { // 推导接口
template<typename Base>
struct type : Base {
void draw() { this->template invoke<0>(*this); } // 接口要求 draw()
};
};
struct Circle {
void draw() { std::cout << "Drawing a Circle" << std::endl; }
};
struct Square {
void draw() { std::cout << "Drawing a Square" << std::endl; }
};
int main() {
using drawable = entt::poly<Drawable>;
drawable instance{Circle{}}; // 自动绑定 Circle::draw() 到 Drawable::draw()
instance->draw(); // 输出:Drawing a Circle
instance = Square{}; // 自动绑定 Square::draw() 到 Drawable::draw()
instance->draw(); // 输出:Drawing a Square
}Circle 和 Square 都有一个 draw() 函数, 与 Drawable 接口的要求完全匹配, 因此, entt::poly 能够在实例化时自动完成绑定
在某些情况下, 自动推导机制无法完成绑定
- 实现类型中的函数名称与接口名称不同
- 实现类型中的函数签名与接口签名不完全一致
- 想要通过 自由函数 free function 或复杂逻辑实现接口
struct Drawable : entt::type_list<void()> { // 显式定义接口
template<typename Base>
struct type : Base {
void draw() { entt::poly_call<0>(*this); }
};
// 定义别名模板,告诉系统如何实现接口
template<typename Type>
using impl = entt::value_list<&Type::render>; // 显式绑定 render()
};
struct MyType {
void render() { std::cout << "Rendering!" << std::endl; }
};
int main() {
using drawable = entt::poly<Drawable>;
drawable instance{MyType{}}; // 使用 impl 完成绑定
instance->draw(); // 输出:Rendering!
}自由函数
#include <entt/poly/poly.hpp>
#include <iostream>
// 定义自由函数
template<typename Type>
void print(Type &self) { self.print(); }
// 定义概念
struct Drawable : entt::type_list<void()> {
template<typename Base>
struct type : Base {
void draw() { entt::poly_call<0>(*this); } // 接口函数 draw()
};
template<typename Type>
using impl = entt::value_list<&print<Type>>; // 将接口绑定到自由函数 print
};
// 定义实现类型
struct Circle {
void print() { std::cout << "Drawing a Circle" << std::endl; }
};
struct Square {
void print() { std::cout << "Drawing a Square" << std::endl; }
};
int main() {
using drawable = entt::poly<Drawable>;
drawable obj = Circle{}; // Circle 满足 Drawable 的概念
obj->draw(); // 调用自由函数 print(Circle&),输出 "Drawing a Circle"
obj = Square{}; // Square 满足 Drawable 的概念
obj->draw(); // 调用自由函数 print(Square&),输出 "Drawing a Square"
return 0;
}这里的 self 参数不是必须的, 如果不需要则可以将其省略
多个接口
struct Drawable : entt::type_list<void(), void(int)> { // 定义两个接口函数
template<typename Base>
struct type : Base {
void draw() { this->template invoke<0>(*this); } // 第 0 个接口函数
void update(int x) { this->template invoke<1>(*this, x); } // 第 1 个接口函数
};
template<typename Type>
using impl = entt::value_list<&Type::draw, &Type::update>; // 分别绑定实现
};
struct MyType {
void draw() { std::cout << "Drawing!" << std::endl; }
void update(int x) { std::cout << "Updating with " << x << std::endl; }
};
int main() {
entt::poly<Drawable> obj = MyType{};
obj->draw(); // 输出:Drawing!
obj->update(42); // 输出:Updating with 42
}类型转换的灵活性
静态虚表 中的函数签名定义了接口的参数类型和返回值类型, 实际实现的函数签名可以略有不同, 但需要保证参数类型和返回值类型能够完成 类型转换
struct Drawable : entt::type_list<void(int)> { // 接口函数需要一个 int 参数
// ...
template<typename Type>
using impl = entt::value_list<&Type::printWithArg>; // 绑定到 printWithArg
};
struct Circle {
void printWithArg(float value) { // 接收 float 参数,但 int 可以隐式转换为 float
std::cout << "Drawing Circle with value: " << value << std::endl;
}
};概念继承 Concept Inheritance
在 entt::poly 中, 概念 Concept 的继承机制允许开发者构建 概念层次结构 Hierarchy, 这使得可以组合和扩展多个概念, 为复杂的接口需求提供灵活的解决方案
概念继承 必须属于同一类 型族, 也就是说要么全是 推导概念 Deduced Concept 或是 显式定义概念 Defined Concept, 不能混用
推导概念的继承
推导概念通过扩展 type 和 impl 来继承
// 基础概念
struct Drawable : entt::type_list<> {
template<typename Base>
struct type : Base {
void draw() { entt::poly_call<0>(*this); } // 定义 draw 接口
};
template<typename Type>
using impl = entt::value_list<&Type::draw>; // 将接口绑定到 Type::draw
};
// 扩展继承概念
struct DrawableAndErasable : entt::type_list<> {
template<typename Base>
struct type : typename Drawable::type<Base> { // 继承 Drawable 的 type
static constexpr auto base = Drawable::impl<Drawable::type<entt::poly_inspector>>::size;
void erase() { entt::poly_call<base + 0>(*this); } // 新增 erase 接口
};
template<typename Type>
using impl = entt::value_list_cat_t<
typename Drawable::impl<Type>, // 继承 Drawable 的实现
entt::value_list<&Type::erase> // 添加 erase 的实现
>;
};
// 实现类型
struct MyType {
void draw() { std::cout << "Drawing!" << std::endl; }
void erase() { std::cout << "Erasing!" << std::endl; }
};
//使用
int main() {
using drawable_and_erasable = entt::poly<DrawableAndErasable>;
drawable_and_erasable obj = MyType{};
obj->draw(); // 输出:Drawing!
obj->erase(); // 输出:Erasing!
return 0;
}-
继承基础概念的
type
DrawableAndErasable::type不直接继承Base, 而是继承了Drawable::type<Base>, 这将Drawable的所有 接口 注入到DrawableAndErasable中 -
计算偏移量
base
使用Drawable::impl获取基础概念的虚表大小, 用作新接口的索引偏移量, 这个base表示基础概念的静态虚表中已有函数的数量 -
定义新的接口
在type中添加了新的接口erase(),erase()使用偏移量base + 0, 表示它是虚表中新增的第一个函数 -
组合实现
impl规则
使用entt::value_list_cat_t将基础概念的实现Drawable::impl<Type>和新函数的实现&Type::erase组合起来, 这生成了完整的实现列表
显式定义概念的继承
template<typename... Type>
entt::type_list<Type...> as_type_list(const entt::type_list<Type...> &);
// 基础概念
struct Drawable : entt::type_list<void()> { // 定义接口函数类型 void()
template<typename Base>
struct type : Base {
void draw() { entt::poly_call<0>(*this); } // 索引 0 对应 draw()
};
template<typename Type>
using impl = entt::value_list<&Type::draw>; // 将接口函数绑定到实现
};
// 扩展继承概念
struct DrawableAndErasable : entt::type_list_cat_t
<
decltype(as_type_list(std::declval<Drawable>())), // 父概念的类型列表
entt::type_list<void()> // 子概念添加的 erase()
>
{
template<typename Base>
struct type : Base {
void draw() { entt::poly_call<0>(*this); } // draw() 继承自父概念
void erase() { entt::poly_call<1>(*this); } // erase() 是新接口
};
template<typename Type>
using impl = entt::value_list_cat_t<
typename Drawable::impl<Type>, // 继承父概念的实现
entt::value_list<&Type::erase> // 添加 erase() 的实现
>;
};
-
基础概念的
type_list
使用decltype(as_type_list(...))提取基础概念Drawable的type_list, 这将 基础概念 的所有函数类型注入到当前概念 -
组合新的类型列表
使用entt::type_list_cat_t将 基础概念 的类型列表与新函数的类型列表组合在一起, 在这里,entt::type_list<void()>定义了一个新函数类型void()
这里的 as_type_list 是一个自定义辅助模版函数, 其主要作用是从一个 概念 (如 Drawable) 中提取其底层的 type_list 类型, 以便进行继承和扩展
template<typename... Type>
entt::type_list<Type...> as_type_list(const entt::type_list<Type...> &);它接收一个 entt::type_list<Type...> 类型的对象, 返回其底层类型列表, 即模板参数 Type...
类型列表 type_list
在 显式定义概念的继承 中, type_list 是 entt::poly 提供的一种类型列表工具, 用来描述概念中接口函数的类型集合, 它是显式定义概念中核心的组成部分, 主要用于记录概念的所有函数类型信息
- 定义接口函数的签名: 包括参数类型、返回值类型以及修饰符 (如
const) - 生成静态虚表: 显式定义概念需要 静态虚表 记录所有函数类型,
type_list就是用来描述这些函数的类型信息 - 支持继承和扩展: 在概念继承中, 通过组合多个
type_list来构建新概念的函数类型集合
type_list 直接用于生成静态虚表, 每个函数类型在虚表中占据一个索引, 这些索引会在调用时被 poly_call 使用
struct DrawableAndErasable : entt::type_list_cat_t
<
decltype(as_type_list(std::declval<Drawable>())),
entt::type_list<void()>
>
{
// ...
};- 父概念的虚表包含
- 索引
0: 对应void draw()
- 索引
- 子概念的虚表扩展为
- 索引
0: 对应void draw() - 索引
1: 对应void erase()
- 索引
当调用 draw() 或 erase() 时, poly_call 会根据索引从虚表中查找对应的函数
静态多态
从前面多处可以看到, 一旦定义了 概念 和 实现, 就可以使用 poly 类模板来包装满足要求的实例
using drawable = entt::poly<Drawable>;
struct circle {
void draw() { /* 绘制圆形的实现 */ }
};
struct square {
void draw() { /* 绘制正方形的实现 */ }
};
// 封装和调用
drawable instance{circle{}}; // 用 circle 初始化
instance->draw(); // 调用 circle::draw()
instance = square{}; // 替换为 square
instance->draw(); // 调用 square::draw()-
封装对象
entt::poly<Drawable>是一个模板类, 用来封装满足Drawable概念的对象 (如circle和square), 通过entt::poly的构造函数, 可以直接将circle{}或square{}封装为Drawable的实例 -
多态调用
封装后的实例通过->操作符调用接口函数 (如draw()), 这种多态性是静态的, 依赖于模板和类型擦除技术, 而不是传统动态多态依赖的虚函数表 -
接口与实现分离
调用instance->draw()时, 接口函数draw()被映射到具体对象 (如circle或square) 的实现函数
entt::poly 提供了多种构造方式以适应不同的需求
- 默认构造
- 拷贝构造和移动构造
- 托管对象和非托管对象的支持
非托管对象
circle shape;
drawable instance{std::in_place_type<circle &>, shape};这里的 std::in_place_type 用来表示要封装的对象类型, 这里是 circle & (引用类型), shape 是一个已存在的对象, instance 只是引用它, 而不负责构造或销毁
非托管对象的特点
entt::poly不会构造或销毁shape对象- 对象的生命周期由用户手动管理
- 避免了不必要的拷贝或动态分配
非拥有拷贝 Non-Owning Copy
可以通过 as_ref() 方法创建一个 非拥有拷贝
drawable other = instance.as_ref();other 是对 instance 的引用, 而不是真正的拷贝, other 和 instance 共用同一个底层对象, 更轻量化, 避免不必要的对象管理
在上面两种情况下, 不会构造任何元素, 也不会负责销毁所引用的对象
完整的例子
#include <entt/poly/poly.hpp>
#include <iostream>
struct Drawable : entt::type_list<void()> {
template<typename Base>
struct type : Base {
void draw() { entt::poly_call<0>(*this); }
};
template<typename Type>
using impl = entt::value_list<&Type::draw>;
};
struct Circle {
void draw() { std::cout << "Drawing a Circle" << std::endl; }
};
struct Square {
void draw() { std::cout << "Drawing a Square" << std::endl; }
};
int main() {
using drawable = entt::poly<Drawable>;
// 托管对象
drawable instance{Circle{}};
instance->draw(); // 输出:Drawing a Circle
instance = Square{};
instance->draw(); // 输出:Drawing a Square
// 非托管对象
Circle shape;
drawable unmanaged{std::in_place_type<Circle &>, shape};
unmanaged->draw(); // 输出:Drawing a Circle
// 非拥有拷贝
drawable other = unmanaged.as_ref();
other->draw(); // 输出:Drawing a Circle
return 0;
}存储大小和对齐要求
entt::poly 是基于 entt::any 实现的, 其核心特点之一是支持 小对象缓冲优化 Small Buffer Optimization, SBO, 允许在编译时定义内部缓冲区的大小和对齐要求, 从而减少动态内存分配的开销
在使用 entt::poly 时, 可以通过模板参数指定\
- 存储缓冲区大小
- 对齐要求
entt::basic_poly<Drawable, sizeof(double[4]), alignof(double[4])>-
sizeof(double[4])
定义缓冲区的大小, 内部对象如果小于等于这个大小, 会存储在缓冲区中 (即不需要动态内存分配), 如果对象超出这个大小, 则会动态分配内存 -
alignof(double[4])
定义缓冲区的对齐要求, 默认值是为给定大小的对象选择最严格的对齐方式
默认值
如果不指定缓冲区大小和对齐要求, entt::poly 使用以下默认值
-
大小:
sizeof(double[2])
大约16字节 (假设double为8字节), 这个默认大小可以容纳大多数小对象 (如整数或指针) -
对齐要求: 最严格的对齐方式
默认对齐要求为适合所有大小小于或等于缓冲区大小的对象
灵活性
允许用户根据应用场景的需求调整缓冲区大小和对齐要求
- 更大的缓冲区
适合存储较大的对象, 减少动态分配 - 更严格的对齐
适合处理需要特定对齐的复杂类型 (如 SIMD 向量)
如果将缓冲区大小设置为 0, 所有对象都会使用动态分配, 无论其大小如何
对齐的含义
对齐(Alignment)是指在内存中存储数据时,数据地址需要满足特定的规则。例如,一个类型的对齐要求为 alignof(T),表示该类型的对象在内存中存储时,起始地址必须是某个固定的倍数。
对齐要求是现代计算机架构中硬件和性能优化的关键部分,正确对齐可以提高访问效率,而错误对齐可能导致性能下降,甚至在某些架构上引发错误。
为什么需要对齐
1. 硬件访问要求
- 大多数 CPU 和内存硬件访问内存时要求数据地址满足对齐规则。
- 例如,一个 4 字节的整数如果对齐到 4 字节边界上,CPU 可以直接读取,而无需额外的逻辑。
2. 性能优化
- 如果对象未对齐,硬件可能需要分多次访问内存才能完整读取数据。
- 对齐好的数据可以更高效地利用 CPU 的缓存和内存访问带宽。
3. 平台差异
- 不同平台和架构对对齐的要求可能不同。
- 有些架构(如 x86)允许未对齐访问,但性能会降低。
- 有些架构(如 ARM 或 SPARC)对未对齐访问会直接抛出异常。