- tuple把一些类型列表的值聚合到了单个值中,给了他们和一个简单的结构体大致相同的功能。类似地很容易想到对应的union类型:将包含一个单个值,但那个值将有一个从一些可能类型中选择的类型。比如一个数据库域可能包含一个整型、浮点数、string或binary blob,但它在任何时间只能包含一个那些类型的确定的值
- 这里开发一个类模板Variant,它动态存储一个给出的可能的值类型的值,类似于C++17标准库的std::variant<>
- Variant是一个discriminated union,即知道当前激活的是哪个可能的值类型,它提供了比等价的union更好的类型安全
- Variant本身是一个可变参数模板,它接受可能的活动值(active value)的类型列表,比如变量
Variant<int, double, string> field;
- 能存储一个int、double和string,但是在某个时刻只有其中的一个值,下面的程序阐述了Variant的行为
// variant/variant.cpp
#include "variant.hpp"
#include <iostream>
#include <string>
int main()
{
Variant<int, double, std::string> field(17);
if (field.is<int>()) {
std::cout << "Field stores the integer "
<< field.get<int>() << '\n';
}
field = 42; // assign value of same type
field = "hello"; // assign value of different type
std::cout << "Field now stores the string '"
<< field.get<std::string>() << "'\n";
}
Field stores the integer 17
Field now stores the string "hello"
- 还可以用成员函数is<T>()检查当前是否包含一个T类型值
存储
- Variant类型的首要的设计方面是如何管理活动值的存储。不同类型可能有不同的大小和alignment要考虑,此外这个variant将需要存储一个discriminator来表明哪个可能的类型是活动值的类型。一个简单(尽管低效)的存储机制直接使用一个tuple
// variant/variantstorageastuple.hpp
template<typename... Types>
class Variant {
public:
Tuple<Types...> storage;
unsigned char discriminator;
};
- 这里discriminator就像一个tuple的动态索引,只有静态索引等于当前discriminator值的元素有一个有效值,因此当discriminator为0时,get<0>(storage)提供活动值的访问,为1时则是get<1>(storage)提供
- 可以在tuple之上建立核心的variant操作is<T>()和get<T>(),然而这样做十分低效,因为variant本身需要等于所有可能值类型大小和的存储,即使某个时刻只有一个活动值。一个更好的方法是重叠每个可能类型的存储,可以通过递归拆分variant为首部和尾部来实现
// variant/variantstorageasunion.hpp
template<typename... Types>
union VariantStorage;
template<typename Head, typename... Tail>
union VariantStorage<Head, Tail...> {
Head head;
VariantStorage<Tail...> tail;
};
template<>
union VariantStorage<> {
};
- 这里union保证有足够的大小和alignment来允许Types中的任何一个类型能在任何时刻被存储。不幸的是,这个union本身很难使用,因为大多用来实现Variant的技术将使用继承,而这对union是不允许的
- 取而代之,将选择一个variant存储的底层表示:一个足够大到存储任何类型和对任何类型有合适alignment的字符数组,用作一个buffer存储活动值。VariantStorage类模板和一个discriminator一起实现这个buffer
// variant/variantstorage.hpp
#include <new> // for std::launder()
template<typename... Types>
class VariantStorage {
using LargestT = LargestType<Typelist<Types...>>;
alignas(Types...) unsigned char buffer[sizeof(LargestT)];
unsigned char discriminator = 0;
public:
unsigned char getDiscriminator() const { return discriminator; }
void setDiscriminator(unsigned char d) { discriminator = d; }
void* getRawBuffer() { return buffer; }
const void* getRawBuffer() const { return buffer; }
template<typename T>
T* getBufferAs() { return std::launder(reinterpret_cast<T*>(buffer)); }
template<typename T>
T const* getBufferAs() const {
return std::launder(reinterpret_cast<T const*>(buffer));
}
};
- 这里使用了Typelist中开发的LargestType元程序来计算buffer大小,确保足够大到存储任何值类型。类似地,alignas包扩展确保buffer将有一个对任何值类型合适的alignment。计算的buffer本质上是上面展示的union的机器表示。可以使用getBuffer()访问一个这个buffer的指针并通过使用显式转换、placement new(创建新值)和显式析构(析构创建的值)控制存储
设计
- 现在有了一个variant存储问题的解决方案,我们设计Variant类型本身。和Tuple类型一样,使用继承对每个Types列表中的类型提供行为,而不同于Tuple的是,这些基类没有存储。相反,每个基类使用CRTP通过最派生的类型访问共享的variant存储。定义如下的VariantChoice类模板提供当variant的活动值为类型T时所需的buffer上的核心操作
// variant/variantchoice.hpp
#include "findindexof.hpp"
template<typename T, typename... Types>
class VariantChoice {
using Derived = Variant<Types...>;
Derived& getDerived() { return *static_cast<Derived*>(this); }
Derived const& getDerived() const {
return *static_cast<Derived const*>(this);
}
protected:
// compute the discriminator to be used for this type
constexpr static unsigned Discriminator =
FindIndexOfT<Typelist<Types...>, T>::value + 1;
public:
VariantChoice() { }
VariantChoice(T const& value); // see variantchoiceinit.hpp
VariantChoice(T&& value); // see variantchoiceinit.hpp
bool destroy(); // see variantchoicedestroy.hpp
Derived& operator= (T const& value); // see variantchoiceassign.hpp
Derived& operator= (T&& value); // see variantchoiceassign.hpp
};
- 模板参数包Types将包含Variant中的所有类型,它允许我们构建Derived类型,并提供向下转换操作getDerived()。Types的另一个有趣使用是找出Types列表中的特别类型T的位置,它通过元函数FindIndexOfT实现
// variant/findindexof.hpp
template<typename List, typename T, unsigned N = 0,
bool Empty = IsEmpty<List>::value>
struct FindIndexOfT;
// recursive case:
template<typename List, typename T, unsigned N>
struct FindIndexOfT<List, T, N, false>
: public IfThenElse<std::is_same<Front<List>, T>::value,
std::integral_constant<unsigned, N>,
FindIndexOfT<PopFront<List>, T, N+1>>
{
};
// basis case:
template<typename List, typename T, unsigned N>
struct FindIndexOfT<List, T, N, true>
{
};
- 这个索引值用于计算对应T的discriminator值,我们之后将返回这个特定的discriminator值。下面的Variant的骨架阐述了Variant、VariantStorage和VariantChoice之间的关系
// variant/variant-skel.hpp
template<typename... Types>
class Variant
: private VariantStorage<Types...>,
private VariantChoice<Types, Types...>...
{
template<typename T, typename... OtherTypes>
friend class VariantChoice; // enable CRTP
...
};
- 每个Variant有一个单个、共享的VariantStorage基类。此外还有一些数目的VariantChoice基类,他们由下面的嵌套包扩展生成
VariantChoice<Types, Types...>...
- 这个实例中有两个扩展:外部扩展通过扩展首个引用到Types,为Types中的每个类型T生成一个VariantChoice基类。内部扩展扩展了第二次出现的Types,此外它传递所有Types中的类型到每个VariantChoice基类。对于一个
Variant<int, double, std::string>
VariantChoice<int, int, double, std::string>,
VariantChoice<double, int, double, std::string>,
VariantChoice<std::string, int, double, std::string>
- 这三个基类对应的discriminator值将为1、2、3。当variant的存储成员discriminator匹配一个特定的VariantChoice基类的discriminator时,基类负责管理活动值
- discriminator值0被保留用于variant不包含值的情况,这是一种奇态(odd state),只有在分配期间抛出异常时才能观察到
- Variant完整定义如下,下节将描述每个成员的实现
// variant/variant.hpp
template<typename... Types>
class Variant
: private VariantStorage<Types...>,
private VariantChoice<Types, Types...>...
{
template<typename T, typename... OtherTypes>
friend class VariantChoice;
public:
template<typename T> bool is() const; // see variantis.hpp
template<typename T> T& get() &; // see variantget.hpp
template<typename T> T const& get() const&; // see variantget.hpp
template<typename T> T&& get() &&; // see variantget.hpp
// see variantvisit.hpp:
template<typename R = ComputedResultType, typename Visitor>
VisitResult<R, Visitor, Types&...> visit(Visitor&& vis) &;
template<typename R = ComputedResultType, typename Visitor>
VisitResult<R, Visitor, Types const&...> visit(Visitor&& vis) const&;
template<typename R = ComputedResultType, typename Visitor>
VisitResult<R, Visitor, Types&&...> visit(Visitor&& vis) &&;
using VariantChoice<Types, Types...>::VariantChoice...;
Variant(); // see variantdefaultctor.hpp
Variant(Variant const& source); // see variantcopyctor.hpp
Variant(Variant&& source); // see variantmovector.hpp
template<typename... SourceTypes>
Variant(Variant<SourceTypes...> const& source); // variantcopyctortmpl.hpp
template<typename... SourceTypes>
Variant(Variant<SourceTypes...>&& source);
using VariantChoice<Types, Types...>::operator=...;
Variant& operator= (Variant const& source); // see variantcopyassign.hpp
Variant& operator= (Variant&& source);
template<typename... SourceTypes>
Variant& operator= (Variant<SourceTypes...> const& source);
template<typename... SourceTypes>
Variant& operator= (Variant<SourceTypes...>&& source);
bool empty() const;
~Variant() { destroy(); }
void destroy(); // see variantdestroy.hpp
};
值查询与提取
- Variant类型最基本的查询是询问它的活动值是否是一个特定类型T,并当类型已知时访问活动值。下面定义的is()成员函数,确定variant当前是否存储一个T类型值
// variant/variantis.hpp
template<typename... Types>
template<typename T>
bool Variant<Types...>::is() const
{
return this->getDiscriminator() ==
VariantChoice<T, Types...>::Discriminator;
}
- 给出一个variant v,v.is<int>()将确定v的活动值是否为类型int,这个检查直接比较variant的存储中的discriminator和对应的VariantChoice基类的Discriminator值
- 如果查找的类型未在列表中在找到,VariantChoice基类将实例化失败,因为FindIndexOfT将不会包含一个value成员,造成is<T>()的编译错误。这样可以防止用户请求用户在该变量中无法存储的类型的错误
- get()成员函数提取一个存储值的引用,它必须提供需要提取的类型(如v.get<int>()),并且只有当variant的活动值是那个类型时才有效
// variant/variantget.hpp
#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>();
}
- 当variant未保存一个值(它的discriminator为0),get()抛出一个EmptyVariant异常
元素初始化、赋值和析构
- 当活动值有类型T时,每个VariantChoice基类负责处理初始化、赋值和析构,这节通过填充VariantChoice类模板的细节开发这些核心操作
初始化
- 我们从一个variant存储的类型的值初始化一个variant,比如从一个double值初始化一个Variant<int, double, string>,这使用接受一个类型T的值的VariantChoice构造函数实现
// variant/variantchoiceinit.hpp
#include <utility> // for std::move()
template<typename T, typename... Types>
VariantChoice<T, Types...>::VariantChoice(T const& value) {
// place value in buffer and set type discriminator:
new(getDerived().getRawBuffer()) T(value);
getDerived().setDiscriminator(Discriminator);
}
template<typename T, typename... Types>
VariantChoice<T, Types...>::VariantChoice(T&& value) {
// place moved value in buffer and set type discriminator:
new(getDerived().getRawBuffer()) T(std::move(value));
getDerived().setDiscriminator(Discriminator);
}
- 在每个情况中,构造函数使用CRTP操作getDerived()访问共享buffer,然后使用一个类型T的新值执行placement new初始化存储。第一个构造函数拷贝构造进来的值,第二个构造函数则是移动构造。然后,构造函数设置discriminator值来表明variant的存储的(动态)类型
- 最终的目标是能从一个任何类型的值初始化variant,甚至可以隐式转换
Variant<int, double, string> v("hello"); // implicitly converted to string
- 为了实现这点,通过引入using声明继承VariantChoice构造函数到Variant本身
using VariantChoice<Types, Types...>::VariantChoice...;
- 实际上,这个using声明生成了从Types中的每个类型T的拷贝或移动的Variant构造函数,对于一个Variant<int, double, string>,构造函数实际上是
Variant(int const&);
Variant(int&&);
Variant(double const&);
Variant(double&&);
Variant(string const&);
Variant(string&&);
析构
- 当Variant初始化时,一个值构造到它的buffer中,destroy操作处理那个值的析构
// variant/variantchoicedestroy.hpp
template<typename T, typename... Types>
bool VariantChoice<T, Types...>::destroy() {
if (getDerived().getDiscriminator() == Discriminator) {
// if type matches, call placement delete:
getDerived().template getBufferAs<T>()->~T();
return true;
}
return false;
}
- 当discriminator匹配时,通过调用使用
->~T()
的对应的析构函数来显式析构buffer的内容
- 只有当discriminator匹配时,VariantChoice::destroy()操作是有用的。然而,我们通常希望销毁存储在variant中的值,而不考虑当前活动的类型。因此,Variant::destroy()调用它的基类中所有的VariantChoice::destroy()操作
// variant/variantdestroy.hpp
template<typename... Types>
void Variant<Types...>::destroy() {
// call destroy() on each VariantChoice base class; at most one will succeed:
bool results[] = {
VariantChoice<Types, Types...>::destroy()...
};
// indicate that the variant does not store a value
this->setDiscriminator(0);
}
- results初始化器中的包扩展确保destroy被调用在每个VariantChoice基类上。这些调用中的至多一个将会成功,导致variant为空,空状态通过设置discriminator值为0来表明
- 数组results本身只提供一个使用初始化列表的上下文,它的实际值是被忽略的。在C++17中,可以使用一个折叠表达式来消除对这个外来变量的需要
// variant/variantdestroy17.hpp
template<typename... Types>
void Variant<Types...>::destroy()
{
// call destroy() on each VariantChoice base class; at most one will succeed:
(VariantChoice<Types, Types...>::destroy() , ...);
// indicate that the variant does not store a value
this->setDiscriminator(0);
}
赋值
// variant/variantchoiceassign.hpp
template<typename T, typename... Types>
auto VariantChoice<T, Types...>::operator= (T const& value) ->
Derived& {
if (getDerived().getDiscriminator() == Discriminator) {
// assign new value of same type:
*getDerived().template getBufferAs<T>() = value;
}
else {
// assign new value of different type:
getDerived().destroy(); // try destroy() for all types
new(getDerived().getRawBuffer()) T(value); // place new value
getDerived().setDiscriminator(Discriminator);
}
return getDerived();
}
template<typename T, typename... Types>
auto VariantChoice<T, Types...>::operator= (T&& value) -> Derived&
{
if (getDerived().getDiscriminator() == Discriminator) {
// assign new value of same type:
*getDerived().template getBufferAs<T>() = std::move(value);
}
else {
// assign new value of different type:
getDerived().destroy(); // try destroy() for all types
new(getDerived().getRawBuffer()) T(std::move(value)); // place new value
getDerived().setDiscriminator(Discriminator);
}
return getDerived();
}
- 和从一个存储值类型初始化一样,每个VariantChoice提供一个赋值运算符来拷贝(或移动)保存的值类型到variant的存储。这些赋值运算符通过下面的using声明继承自Variant
using VariantChoice<Types, Types...>::operator=...;
- 这个赋值运算符的实现有两条路径。如果variant已经保存了一个给出的T类型值,赋值运算符将直接拷贝或移动赋值T类型值到buffer,discriminator不变。如果variant没有保存一个T类型值,赋值要求一个两步过程:使用Variant::destroy()析构现有值,然后使用placement new初始化一个T类型的新值,设置合适的discriminator
- 这样一个使用placement new的两步赋值有三个常见问题
自赋值
- 自赋值能发生于一个用于variant v的如下表达式
v = v.get<T>()
- 使用上述两步赋值的实现,源值将在拷贝前被析构,导致内存崩溃。幸运的是,自赋值总是意味着discriminator匹配,所以这样的代码将调用T的赋值操作符,而不是这个两步过程
异常
- 如果现有值的销毁已经完成,但是新值的初始化抛出异常,那么variant的状态是什么?在我们的实现中,Variant::destroy()将discriminator值重置为0。在非异常情况下,初始化完成后将适当地设置discriminator。当在初始化新值时发生异常时,discriminator保持0以表明variant未保存值。在我们的设计中,这是产生一个没有value的variant的唯一方法
- 下面的程序阐述了拷贝构造函数抛出异常时,通过拷贝一个类型的值触发一个没有存储的variant
// variant/variantexception.cpp
#include "variant.hpp"
#include <exception>
#include <iostream>
#include <string>
class CopiedNonCopyable : public std::exception
{
};
class NonCopyable
{
public:
NonCopyable() {
}
NonCopyable(NonCopyable const&) {
throw CopiedNonCopyable();
}
NonCopyable(NonCopyable&&) = default;
NonCopyable& operator= (NonCopyable const&) {
throw CopiedNonCopyable();
}
NonCopyable& operator= (NonCopyable&&) = default;
};
int main()
{
Variant<int, NonCopyable> v(17);
try {
NonCopyable nc;
v = nc;
}
catch (CopiedNonCopyable) {
std::cout << "Copy assignment of NonCopyable failed." << '\n';
if (!v.is<int>() && !v.is<NonCopyable>()) {
std::cout << "Variant has no value." << '\n';
}
}
}
Copy assignment of NonCopyable failed.
Variant has no value.
- 访问一个没有值的variant将抛出EmptyVariant异常来允许程序从这个异常条件修复。empty()成员函数检查variant是否为空
// variant/variantempty.hpp
template<typename... Types>
bool Variant<Types...>::empty() const {
return this->getDiscriminator() == 0;
}
std::launder()
- 两步分配的第三个问题是C++标准化委员会在C++ 17标准化过程结束时才意识到的一个微妙问题。C++编译器通常旨在产生高性能代码,而提高生成代码性能的主要机制可能是避免重复从内存到寄存器的数据复制。为了做到这一点,编译器必须做出一些假设,其中一种假设是某些类型的数据在其生命周期内是不可变的。这包括const数据、引用(可以初始化,但之后不修改)以及存储在多态对象中的一些bookkeeping data,这些对象用于调度虚拟函数、定位虚拟基类、以及处理typeid和dynamic_cast操作符
- 上面两步分配过程的问题是,它以一种编译器可能无法识别的方式悄悄地结束一个对象的生命周期,并在同一位置开始另一个对象的生命周期。因此,编译器可能假定它从Variant对象的先前状态获取的值仍然有效,而实际上,使用placement new的初始化使其无效。如果没有缓解,最终的结果是使用具有不可变数据成员的各种类型Variant的程序在编译时偶尔会产生无效的结果,从而获得良好的性能。这种bug通常很难追踪(部分原因是它们很少发生,部分原因是它们在源代码中并不真正可见)
- C++17中,这个问题的解决方案是通过std::launder()访问新对象的地址,它只返回它的实参,但是这使得编译器识别到结果的地址指向一个对象,该对象可能不同于编译器对传递给std::launder()的实参所做的假设。但注意std::launder()只修复它返回的地址,而不是传递给std::launder()的实参,因为编译器根据表达式进行推理,而不是实际的地址(因为它们直到运行时才存在)。因此,在使用placement new构造新值之后,我们必须确保以下每个访问都使用已清洗的(laundered)数据。这就是总是launder指针到Variant buffer的原因。有一些方法可以做得更好(比如添加一个额外的指向buffer的指针成员,并在每次使用placement new赋值一个新值时获得已清洗的地址),但是这些方法使代码变得复杂,难以维护。我们的方法简单而正确,只要通过getBufferAs()成员访问buffer
- std::launder()的情况并不完全令人满意:它非常微妙,难以感知(例如,直到书出版之前我们才注意到它),并且难以缓解(即std::launder()不容易使用)。因此,委员会的几个成员要求做更多的工作来找到一个更令人满意的解决方案
访问者(Visitor)
- is()和get()成员函数允许检查活动值是否为一个特定类型并且能访问一个那个类型的值。然而,检查一个variant中的所有可能类型将快速造成一个冗长的if语句链,比如下面打印一个名为v的Variant<int, double, string>的值
if (v.is<int>()) {
std::cout << v.get<int>();
}
else if (v.is<double>()) {
std::cout << v.get<double>();
}
else {
std::cout << v.get<string>();
}
- 为了推广这个以打印存储在任意variant中的值,需要一个带有辅助的递归实例化的函数模板
// variant/printrec.cpp
#include "variant.hpp"
#include <iostream>
template<typename V, typename Head, typename... Tail>
void printImpl(V const& v)
{
if (v.template is<Head>()) {
std::cout << v.template get<Head>();
}
else if constexpr (sizeof...(Tail) > 0) {
printImpl<V, Tail...>(v);
}
}
template<typename... Types>
void print(Variant<Types...> const& v)
{
printImpl<Variant<Types...>, Types...>(v);
}
int main() {
Variant<int, short, float, double> v(1.5);
print(v);
}
- 对于一个相对简单的操作来说,这是一个相当大的代码量。为了简化这点,我们通过用visit()操作扩展Variant来解决问题。客户随后传递进一个visitor函数对象,它的operator()将被活动值调用。因为活动值可以是variant的任何潜在类型,所以这个operator()可能被重载,或者本身是函数模板。例如,泛型lambda提供模板化的operator()允许简明地表示variant v的打印操作
v.visit([](auto const& value) {
std::cout << value;
});
- 这个泛型lambda大致等同于以下函数对象,这对于还不支持泛型lambda的编译器也是有用的
class VariantPrinter {
public:
template<typename T>
void operator()(T const& value) const
{
std::cout << value;
}
};
- visit()操作的核心类似于递归print操作:它遍历Variant的类型,检查活动值是否具有给定类型(用is<T>()),然后在找到适当类型时进行操作
// variant/variantvisitimpl.hpp
template<typename R, typename V, typename Visitor,
typename Head, typename... Tail>
R variantVisitImpl(V&& variant, Visitor&& vis, Typelist<Head, Tail...>) {
if (variant.template is<Head>()) {
return static_cast<R>(
std::forward<Visitor>(vis)(
std::forward<V>(variant).template get<Head>()));
}
else if constexpr (sizeof...(Tail) > 0) {
return variantVisitImpl<R>(std::forward<V>(variant),
std::forward<Visitor>(vis),
Typelist<Tail...>());
}
else {
throw EmptyVariant();
}
}
- variantVisitImpl()是一个具有多个模板参数的非成员函数模板,模板参数R描述访问操作的结果类型,之后将返回该结果类型。V是variant的类型,Vsitor是访问者的类型,Head和Tail用于分解Variant中的类型以实现递归
- 第一个if执行一个(运行时)检查以确定给定variant的活动值是否为Head类型:如果是,则通过get<Head>()从variant中提取值并将其传递给访问者,从而终止递归。当需要考虑更多元素时,第二个if执行递归。如果没有匹配任何类型,则variant不包含值,在这种情况下,实现抛出EmptyVariant异常
- 除了VisitResult提供的结果类型计算,visit()实现很直接简单
// variant/variantvisit.hpp
template<typename... Types>
template<typename R, typename Visitor>
VisitResult<R, Visitor, Types&...>
Variant<Types...>::visit(Visitor&& vis)& {
using Result = VisitResult<R, Visitor, Types&...>;
return variantVisitImpl<Result>(*this, std::forward<Visitor>(vis),
Typelist<Types...>());
}
template<typename... Types>
template<typename R, typename Visitor>
VisitResult<R, Visitor, Types const&...>
Variant<Types...>::visit(Visitor&& vis) const& {
using Result = VisitResult<R, Visitor, Types const &...>;
return variantVisitImpl<Result>(*this, std::forward<Visitor>(vis),
Typelist<Types...>());
}
template<typename... Types>
template<typename R, typename Visitor>
VisitResult<R, Visitor, Types&&...>
Variant<Types...>::visit(Visitor&& vis) && {
using Result = VisitResult<R, Visitor, Types&&...>;
return variantVisitImpl<Result>(std::move(*this),
std::forward<Visitor>(vis),
Typelist<Types...>());
}
- 实现直接委托给variantVisitImpl,传递variant本身,转发访问者,并提供完整的类型列表。三种实现之间的唯一区别在于它们是否将variant本身传递为Variant&、Variant const&或Variant&&
访问结果类型
- visit()的结果类型仍然是个谜。一个给定的访问者可能具有不同的operator()重载以产生不同的结果类型,模板化的operator的结果类型取决于参数类型或其某种组合。例如,考虑下面的泛型lambda
[](auto const& value) {
return value + 1;
}
- 这个lambda的结果类型取决于输入类型:给定一个int则生成一个int,给定一个double则生成一个double,如果这个lambda被传递给一个Variant<int, double>的visit()操作,结果应该是什么?没有一个正确的答案,所以visit()操作允许显式提供结果类型。例如,可能想捕获另一个Variant<int, double>中的结果,可以显式指定结果类型作为visit()的第一个模板实参
v.visit<Variant<int, double>>([](auto const& value) {
return value + 1;
});
- 当没有合适的解决方案时,显式指定结果类型的能力是很重要的。但是,要求在所有情况下显式指定结果类型则过于累赘。因此visit()使用默认模板实参和一个简单元程序的组合来提供这两种选项。回想起visit()的声明
template<typename R = ComputedResultType, typename Visitor>
VisitResult<R, Visitor, Types&...> visit(Visitor&& vis) &;
- 在上面的示例中显式指定的模板参数R也有一个默认实参,因此不是总需要显式指定。默认实参是一个不完整的哨兵类型ComputedResultType
class ComputedResultType;
- 为了计算它的结果类型,visit把所有模板参数传递给VisitResult,一个提供对新类型trait VisitResultT的访问的别名模板
// variant/variantvisitresult.hpp
// an explicitly-provided visitor result type:
template<typename R, typename Visitor, typename... ElementTypes>
class VisitResultT
{
public:
using Type = R;
};
template<typename R, typename Visitor, typename... ElementTypes>
using VisitResult =
typename VisitResultT<R, Visitor, ElementTypes...>::Type;
- VisitResultT的主要定义处理R的实参已被明确指定的情况,因此Type被定义为R。当R接收其默认实参CabultReultType时,一个分离的局部特化将被应用
template<typename Visitor, typename... ElementTypes>
class VisitResultT<ComputedResultType, Visitor, ElementTypes...>
{
...
}
通用结果类型(Common Result Type)
- 当调用一个可能为每个variant的元素类型生成不同类型的访问者时,如何将这些类型组合成用于visit()的单个结果类型?有一些明显的情况——如果访问者为每个元素类型返回相同的类型,那么它应该是visit()的结果类型
- C++已经有了一个合理的结果类型的概念:在三元表达式
b ? x : y
中,表达式的类型是x和y类型之间的通用类型(common type),例如,如果x具有int类型,y具有double类型,则通用类型是double,因为int会提升为double。我们可以在一个type trait中捕获通用类型的概念
// variant/commontype.hpp
using std::declval;
template<typename T, typename U>
class CommonTypeT
{
public:
using Type = decltype(true? declval<T>() : declval<U>());
};
template<typename T, typename U>
using CommonType = typename CommonTypeT<T, U>::Type;
- 通用类型的概念扩展为一系列类型:通用类型是集合中的所有类型都可以提升的类型。对于访问者,希望计算访问者在使用variant中的每个类型调用时生成结果类型的通用类型
// variant/variantvisitresultcommon.hpp
#include "accumulate.hpp"
#include "commontype.hpp"
// the result type produced when calling a visitor with a value of type T:
template<typename Visitor, typename T>
using VisitElementResult = decltype(declval<Visitor>()(declval<T>()));
// the common result type for a visitor called with each of the given element types:
template<typename Visitor, typename... ElementTypes>
class VisitResultT<ComputedResultType, Visitor, ElementTypes...>
{
using ResultTypes =
Typelist<VisitElementResult<Visitor, ElementTypes>...>;
public:
using Type =
Accumulate<PopFront<ResultTypes>, CommonTypeT, Front<ResultTypes>>;
};
- VisitResult计算分两个阶段进行。首先,VisitElementResult计算使用T类型值调用访问者时产生的结果类型。这个元函数应用于每个给定的元素类型,以确定访问者可以生成的所有结果类型,在类型列表ResultTypes中捕获结果
- 接下来,计算使用Accumulate算法将通用类型计算应用于结果类型的typelist,它的初始值(Accumulate的第三个实参)是第一个结果类型,它通过CommonTypeT与ResultTypes typelist的其余部分的连续值组合。最终结果是所有访问者的结果类型能转换的通用类型,而如果结果类型不兼容则出现错误
- C++11开始,标准库提供了对应的type trait std::common_type<>,它实际上使用组合CommonTypeT和Accumulate的方法来生成任意数量传递类型的通用类型。通过使用std::common_type<>,VisitResultT的实现变得更简单
// variant/variantvisitresultstd.hpp
template<typename Visitor, typename... ElementTypes>
class VisitResultT<ComputedResultType, Visitor, ElementTypes...>
{
public:
using Type =
std::common_type_t<VisitElementResult<Visitor, ElementTypes>...>;
};
- 下面的例子打印传递进一个给得到值加1的泛型lambda生成的类型
// variant/visit.cpp
#include "variant.hpp"
#include <iostream>
#include <typeinfo>
int main()
{
Variant<int, short, double, float> v(1.5);
auto result = v.visit([](auto const& value) {
return value + 1;
});
std::cout << typeid(result).name() <<'\n';
}
- 这个程序的输出将会是double的type_info,因为double是所有结果类型能转换的类型
Variant初始化和赋值
- Variant可以用多种方法初始化和赋值,包括默认构造、拷贝和移动构造、拷贝和移动赋值
默认初始化
- 一个可能的语法会是通过discriminator 0来表示没有存储值。然而这样的空variant不是通用的(例如,不能访问它们或找到任何要提取的值),并且使这种默认初始化行为将空variant的异常状态提升到了通用状态
- 可选的是,默认的构造函数能构造一个some类型值。对于variant,遵循C++17的 std::variant<>和默认构造一个类型列表中首个类型值的语法
// variant/variantdefaultctor.hpp
template<typename... Types>
Variant<Types...>::Variant() {
*this = Front<Typelist<Types...>>();
}
- 这个方法是简单和可预测的,避免了在大多数使用时空variant的引入。这种行为如下
// variant/variantdefaultctor.cpp
#include "variant.hpp"
#include <iostream>
int main()
{
Variant<int, double> v;
if (v.is<int>()) {
std::cout <<"Default-constructed v stores the int "
<< v.get<int>() <<'\n';
}
Variant<double, int> v2;
if (v2.is<double>()) {
std::cout <<"Default-constructed v2 stores the double "
<< v2.get<double>() <<'\n';
}
}
Default-constructed v stores the int 0
Default-constructed v2 stores the double 0
拷贝/移动初始化
- 要拷贝一个source variant,需要确定它当前存储的类型,将该值拷贝构造到buffer中,并设置discriminator。幸运的是,visit()处理解码source variant的活动值,并且从VariantChoice继承的拷贝赋值运算符将拷贝构造值到buffer中,从而实现简洁紧凑的实现
// variant/variantcopyctor.hpp
template<typename... Types>
Variant<Types...>::Variant(Variant const& source) {
if (!source.empty()) {
source.visit([&](auto const& value) {
*this = value;
});
}
}
- 移动构造是相似的,不同的只是在访问source variant和从源值移动赋值时使用的是std::move
// variant/variantmovector.hpp
template<typename... Types>
Variant<Types...>::Variant(Variant&& source) {
if (!source.empty()) {
std::move(source).visit([&](auto&& value) {
*this = std::move(value);
});
}
}
- 一个特别有趣的方面是,visitor-based实现也适用于拷贝和移动操作的模板化形式。例如,模板化的拷贝构造函数可以定义如下
// variant/variantcopyctortmpl.hpp
template<typename... Types>
template<typename... SourceTypes>
Variant<Types...>::Variant(Variant<SourceTypes...> const& source) {
if (!source.empty()) {
source.visit([&](auto const& value) {
*this = value;
});
}
}
- 因为这个代码访问source,所以对于source variant的每种类型都会引发对*this的赋值。这个赋值的重载解析将为每个源类型找到最合适的目标类型,必要时执行隐式转换。下面的示例阐述了来自不同variant类型的构造和赋值
// variant/variantpromote.cpp
#include "variant.hpp"
#include <iostream>
#include <string>
int main()
{
Variant<short, float, char const*> v1((short)123);
Variant<int, std::string, double> v2(v1);
std::cout << "v2 contains the integer " << v2.get<int>() << '\n';
v1 = 3.14f;
Variant<double, int, std::string> v3(std::move(v1));
std::cout << "v3 contains the double " << v3.get<double>() << '\n';
v1 ="hello";
Variant<double, int, std::string> v4(std::move(v1));
std::cout << "v4 contains the string " << v4.get<std::string>() << '\n';
}
- 从v1构造或赋值到v2或v3涉及整型提升(short到int),浮点提升(float到double),以及用户定义的转换(char const*到std::string)。这个程序的输出如下
v2 contains the integer 123
v3 contains the double 3.14
v4 contains the string hello
赋值
- Variant赋值运算符与拷贝和移动操作类似,这里只阐述拷贝赋值运算符
// variant/variantcopyassign.hpp
template<typename... Types>
Variant<Types...>& Variant<Types...>::operator= (Variant const&
source) {
if (!source.empty()) {
source.visit([&](auto const& value) {
*this = value;
});
}
else {
destroy();
}
return *this;
}
- 这里唯一有趣的附加项是else分支:当source variant不包含值时(通过discriminator 0表明),析构目标值,隐式设置它的discriminator为0
网友评论