26 可识别联合体
可识别联合体中存储了一组可能类型之一的值,但是和传统的联合体不同的是可识别联合体知道当前其中存储的是什么类型,也就可以提高安全性。本章实现了Variant<>
,类似于std::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";
}
26.1 存储
可以分别通过Tuple<>
和union
来实现可识别联合体:
// variant/variantstorageastuple.hpp
template<typename... Types>
class Variant {
public:
Tuple<Types...> storage;
unsigned char discriminator;
};
// 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<> {
};
但是Tuple<>
的方法占用了过多的存储空间,union
的方式无法使用继承,书中最终采用的方式是使用一个足够大的缓冲区来容纳所有可能的类型,并通过VariantStorage::discriminator
标识缓冲区中的数据应该解释为哪种类型(下标从1开始,0表示未存储数据),代码如下:
// 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));
}
};
26.2 设计
可识别联合体的定义如下:
// 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
};
// 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>
{
};
// 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
};
基类VariantStorage<Types...>
为可识别的联合体提供存储,其余基类VariantChoice<Types, Types...>...
为可识别联合体提供标识——这是一种嵌套的展开参数包的方式,展开后将继承多个VariantChoice<>
,其中第一个模板参数是Types
中的每一个类型,第二个模板参数是Types
中的所有类型。将Type...
作为第二个模板参数传入到VariantChoice<>
中以使getDrived()
返回派生类的引用用到了CRTP技术。
Variant<>
中的成员函数将在后面的小节中详细说明。
26.3 查询和提取
查询操作判断可识别联合体中存储的是否是某一种类型的值:
// variant/variantis.hpp
template<typename... Types>
template<typename T>
bool Variant<Types...>::is() const
{
return this->getDiscriminator() == VariantChoice<T, Types...>::Discriminator;
}
提取操作从可识别联合体中获取某种类型的值:
// 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>();
}
26.4 通过某种类型的值初始化、赋值和析构
26.4.1 初始化
可识别联合体可以通过某种类型的初值初始化,为此通过using VariantChoice<Types, Types...>::VariantChoice...
拉取了所有基类的构造函数,例如Variant<int, double, string>
继承了下列构造函数:
Variant(int const&);
Variant(int&&);
Variant(double const&);
Variant(double&&);
Variant(string const&);
Variant(string&&);
所以只需要实现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);
}
这里使用的是原地new
运算符,即在指针所指向的内存中构造T
类型的对象。
26.4.2 析构
析构时需要根据当前存储的类型释放缓冲区:
// 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;
}
这里的析构也是原地析构,即析构后不释放缓冲区。因为可识别联合体中存储的肯定是某种类型的数据,所以有且只有一个VariantChoice<Types, Types...>::destroy()
返回true
。为了强制析构,可以将所有的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);
}
析构的最后将VariantStorage::discriminator
设置为了0,表示可识别联合体中未存储任何类型的值。
在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);
}
26.4.3 赋值
可识别联合体初始化后也可以通过某种类型的值进行赋值,为此也通过using VariantChoice<Types, Types...>::operator=...
拉取了基类中的赋值运算符:
// 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();
}
自赋值问题
移动赋值运算符一般要考虑自赋值的问题,因为当旧值和新值相同时,销毁后赋值的操作可能导致内存问题。不过在可识别联合体中不存在这个问题,因此类型相同时将调用类型的移动构造函数,自赋值的问题将被该类型的移动构造函数解决。
赋值抛出异常的问题
如果原地析构正常完成而原地构造抛出了异常,则可识别联合体将处于未存储值的状态,例子如下:
// 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&) {616 Chapter 26: Discriminated Unions
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';
}
}
}
判断可识别联合体是否存储值的empty()
定义如下:
// variant/variantempty.hpp
template<typename... Types>
bool Variant<Types...>::empty() const {
return this->getDiscriminator() == 0;
}
std::launder
提高运行效率的方法之一是尽量避免在内存中复制值,为此编译器将假设某些值在一段时间之内是不会变的,但是可识别联合体中的原地析构和构造可能让编译器做出错误的判断,从而引发各种bug。
为此C++17提供了std::launder()
的解决办法,它以一个地址作为参数,只是简单的返回该地址,同时提示编译器这两个地址所存储的对象可能已经不同了。原文:
Since C++17, the solution for this issue is to access the address of the new object through std::launder(), which just returns its argument, but which causes the compiler to recognize that the resulting address points to an object that may differ from what the compiler assumes about the argument passed to std::launder().
26.5 访问函数
26.3中的is()
和get()
可以用来查询和提取可识别联合体中的类型,但是这将会导致冗长的判断语句,例如打印可识别联合体:
Variant<int, double, string> v;
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/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);
}
如果为每一个简单的操作都实现类似的函数则要编写很多代码,解决办法是将访问函数作为参数:
// 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();
}
}
// 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...>());
}
26.5.1 访问函数的返回类型
如果访问函数有返回类型,就要保证无论可识别联合体中是什么类型,访问函数的返回值都可以转换为该类型,否则将不能编译,为此需要实现VisitResult<>
:
// 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;
在指定了返回类型R
的情况下,返回类型就是R
,而当未指定返回类型时(也就是R
仍然为默认模板实参ComputedResultType
,参见26.2),就要推导可识别联合体中所有可能类型的值调用访问函数的返回类型的公共类型。
26.5.2 公共返回类型
推导所有可能类型的公共返回类型的模板代码为:
// 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/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>>;
};
VisitElementResult<>
得到所有可能的返回类型形成类型列表,最终通过类型列表的累加算法Accumulate<>
推导公共返回类型。当然也可以使用std::is_common_type_t
来简化:
// variant/variantvisitresultstd.hpp
template<typename Visitor, typename... ElementTypes>
class VisitResultT<ComputedResultType, Visitor, ElementTypes...>
{
public:
using Type =
std::common_type_t<VisitElementResult<Visitor, ElementTypes>...>;
};
最后是测试代码:
// 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';
}
26.6 可识别联合体的初始化和赋值
默认构造函数
可识别联合体可以被默认构造,但是并不是不存储值,而是用第一个类型的默认值初始化,这样就尽量避免了不存储值的特殊情况:
// variant/variantdefaultctor.hpp
template<typename... Types>
Variant<Types...>::Variant() {
*this = Front<Typelist<Types...>>();
}
测试代码如下:
// 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';
}
}
拷贝和移动初始化
拷贝初始化最终转换为了26.4.1中的初始化:
template<typename... Types>
Variant<Types...>::Variant(Variant const& source) {
if (!source.empty()) {
source.visit([&](auto const& value) {
*this = value;
});
}
}
移动初始化类似:
// variant/variantmovector.hpp
template<typename... Types>
Variant<Types...>::Variant(Variant&& source) {
if (!source.empty()) {
std::move(source).visit([&](auto&& value) {
*this = std::move(value);
});
}
}
从另一个类型的可识别联合体初始化:
// 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;
});
}
}
这种初始化方式可能会发生类型转换以匹配构造函数,测试代码如下:
// 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);26.6 Variant Initialization and Assignment 627
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';
}
赋值
赋值和构造类似,书中只有拷贝构造:
// 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;
}
26.7 后记
本章中的实现的Variant<>
在实例化时不能使用相同的类型,但是std::variant
可以。