CTTCG 26 Discriminated Unions
本章想要实现的类模板相当于 std::variant
。 可以用递归 union 模板,也可以用字符数组存储,本章选用的是后者。
使用字符数组存储未知元素,需要注意:
使用
alignas(Types...)
属性标识字符数组的对齐大小,否则强制类型转换后对齐大小和目的类型可能不相符。使用 返回 后的指针。
std::launder
可以提示编译器某个地址中的值发生了改变,甚至类型都可能变化了,以避免编译器分析变量的生命周期并进行激进的优化。
[basic.life]/8 tells us that, if you allocate a new object in the storage of the old one, you cannot access the new object through pointers to the old. launder allows us to side-step that.
数据结构
书中的实现是让 Variant
继承了 VariantStorage
模板和 VariantChoice
模板。
- 前者提供存储空间、
discriminator
(标识当前存储的是哪个类型)和转换成任何指针的能力(通过std::launder
禁止激进优化)。 - 后者使用 CRTP 提供一堆构造、赋值函数(Variant 中需要用
using
pack expansion 导入),以及析构所用的destory()
函数。为了让 CRTP 正常工作,Variant
模板需要将VariantChoice
模板声明为友元。
Each VariantChoice base class is responsible for handling the initialization, assignment, and destruction when the active value has type T.
功能实现
is<T>()
用来判断是否为对应类型。获取 discriminator
和 T
类型所在的 index 比较即可(找一个类型所在的 index 和在 Typelist 和 Tuple 实现是一样的)。
get<T>()
用来获取某个类型的值。首先检查是否为指定的类型,如果不是,则抛出异常,是则通过 VariantStorage
提供的方法将字符数组首地址强制转换成对应类型的指针,然后再解引用。
#include <exception>
class EmptyVariant : public std::exception {};
template<typename... Types>
template<typename T>
T& Variant<Types...>::get() & {
if (empty()) {
throw EmptyVariant();
}
assert(is<T>());
return *this->template getBufferAs<T>();
}
注意 getBufferAs
是模板函数,所以要加 template
关键字标识。
构造
从 VariantChoice
中继承。
析构
书中的方法是调用所有基类的析构函数。只有下标命中的析构函数会真正执行析构的逻辑。
感觉可以存储一个析构函数指针:void (*destory)(void *);
然后每个 VariantChoice
基类中都设置一个静态的同签名的析构函数。这样析构的时候可以直接调用而不必扫描列表。判断是否为该类型的时候,可以直接比较析构函数指针的地址而不必使用下标。不过其他情况是否必须要使用下标还待考量。
赋值
赋值操作符的版本通过参数类型来选择。在赋值操作符的实现中需检查当前存储的是否为本类元素。如果不是本类元素则需要析构再 placement new;是的话则进行本类元素的赋值操作。
Visitor
书中使用了一种递归实现(有可能被优化内联成一个但函数)。这种实现逐一检查当前的存储种类,遇到匹配的就调用 visitor 并返回结果。如全都没有匹配则抛出异常。
低抽象层次的 union 和整型下标可以使用 case 关键字完成跳转,比连续的 if 检查要快。
libstdc++ std::variant
的 visit 函数模板会为 11 个以内参数数量的 std::variant
使用 case 跳转,而为参数数量极多的类型生成函数指针数组并跳转。
默认构造
和标准库的 std::variant
相同,本章节实现的 Variant
在默认构造时会构造第一个参数的默认值。
从同类型(Variant
)复制构造
书中使用 visit
方法逐个查询真实的存储类型,然后完成赋值操作。
从同类型(Variant
)赋值
和复制构造差不多,也是借助 visit
方法完成派发。