23 标签联合

  • 这里开发一个类似于std::variant的类模板Variant,Variant提供了比union更好的类型安全,其行为如下
#include "variant.hpp"
#include 
#include 

int main()
{
  Variant v(42);
  if (v.is()) std::cout << v.get(); // 42
  v = "hi";
  std::cout << v.get(); // hi
}

存储

  • Variant的首要设计是管理活动值的存储,不同类型可能有不同的大小和对齐要考虑,一个简单(尽管低效)的存储机制是直接使用一个tuple。此外还需要存储一个discriminator作为tuple的索引,比如discriminator为0时,get<0>(storage)访问活动值
template
class Variant {
 public:
  Tuple storage;
  unsigned char discriminator;
};
  • 但这种做法导致Variant需要所有类型大小总和的空间,即使某个时刻只有一个活动值。更好的做法是重叠每个可能类型的空间,这可以通过递归拆分variant为首部和尾部来实现
template
union VariantStorage;

template
union VariantStorage {
  Head head;
  VariantStorage tail;
};

template<>
union VariantStorage<> {};
  • 这里的union保证足够的大小和对齐来存储参数列表的任意类型,但这个union本身很难使用,因为大多用来实现Variant的技术将使用继承,而这对union是不允许的。取代做法是用最底层的字符数组来存储,这个数组足够大到存储任何类型(通过一个计算最大类型的type traits),并对任何类型有合适的对齐(通过alignas指定符)
#include  // for std::launder()

template
class VariantStorage {
  using LargestT = LargestType>; 
  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
  T* getBufferAs() { return std::launder(reinterpret_cast(buffer)); }

  template
  const T* getBufferAs() const
  {
    return std::launder(reinterpret_cast(buffer));
  }
};

设计

  • 接着设计Variant类型本身。和Tuple一样,使用继承对每个Types中的类型提供行为。不同于Tuple的是,这些基类没有存储。相反,每个基类使用CRTP通过最派生的类型访问共享的Variant存储。定义如下VariantChoice类模板,它提供活动值为类型T时字符数组上所需的核心操作
template
class VariantChoice {
  using Derived = Variant;
  Derived& getDerived() { return *static_cast(this); }
  const Derived& getDerived() const
  {
    return *static_cast(this);
  }
 protected:
  constexpr static unsigned Discriminator =
    FindIndexOfT, T>::value + 1;
 public:
  VariantChoice() {}
  // see variantchoiceinit.hpp
  VariantChoice(const T& value);
  VariantChoice(T&& value);

  bool destroy(); // see variantchoicedestroy.hpp
  // see variantchoiceassign.hpp
  Derived& operator=(const T& value);
  Derived& operator=(T&& value);
};
  • 元函数FindIndexOfT用来找出Types中类型T的位置,实现如下
template::value>
struct FindIndexOfT;

// recursive case:
template
struct FindIndexOfT
: public IfThenElse, T>::value,
  std::integral_constant,
  FindIndexOfT, T, N+1>>
{};

// basis case:
template
struct FindIndexOfT
{};
  • Variant继承于VariantStorage和VariantChoice
template
class Variant : private VariantStorage,
  private VariantChoice...
{
  template
  friend class VariantChoice; // enable CRTP
  ...
};
  • 对于一个
Variant
  • 生成下列VariantChoice基类
VariantChoice,
VariantChoice,
VariantChoice
  • 这三个基类对应的discriminator值将为1、2、3。当Variant的存储成员discriminator匹配一个特定基类的VariantChoice::Discriminator时,基类负责管理活动值
  • discriminator值为0被保留用于Variant不包含值的情况,这是一种奇态(odd state),只有在分配期间抛出异常时才能观察到
  • Variant完整定义如下
template
class Variant : private VariantStorage,
  private VariantChoice...
{
  template
  friend class VariantChoice;
 public:
  template bool is() const; // see variantis.hpp

  // see variantget.hpp
  template T& get() &;
  template const T& get() const&;
  template T&& get() &&;

  // see variantvisit.hpp:
  template
  VisitResult visit(Visitor&& vis) &;

  template
  VisitResult visit(Visitor&& vis) const&;

  template
  VisitResult visit(Visitor&& vis) &&;

  using VariantChoice::VariantChoice...;

  Variant(); // see variantdefaultctor.hpp
  Variant(const Variant& source); // see variantcopyctor.hpp
  Variant(Variant&& source); // see variantmovector.hpp

  template
  Variant(const Variant& source); // see variantcopyctortmpl.hpp

  template
  Variant(Variant&& source); // see variantmovectortmpl.hpp

  using VariantChoice::operator=...;

  Variant& operator= (const Variant& source); // see variantcopyassign.hpp
  Variant& operator= (Variant&& source); // see variantmoveassign.hpp

  template
  Variant& operator= (const Variant& source); // see variantcopyassigntmpl.hpp

  template
  Variant& operator= (Variant&& source); // see variantmoveassigntmpl.hpp

  bool empty() const; // see variantempty.hpp

  ~Variant() { destroy(); }
  void destroy(); // see variantdestroy.hpp
};

查询

  • is()成员函数定义如下,用于确定Variant当前是否存储一个T类型值
// variant/variantis.hpp

template
  template
bool Variant::is() const
{
  return this->getDiscriminator() ==
    VariantChoice::Discriminator;
}

// v.is()将确定v的活动值是否为int
  • 如果查找的类型未在列表中在找到,FindIndexOfT将不会包含一个value成员,于是VariantChoice基类将实例化失败,从而造成is()的编译错误,这样就可以防止用户请求无法存储的类型

提取

  • get()成员函数通过接受一个类型,提取一个该类型存储值的引用,并且仅当Variant的活动值是该类型时才有效。当Variant未保存一个值(即discriminator为0)时,get()抛出一个EmptyVariant异常
// variant/variantget.hpp

#include 
class EmptyVariant : public std::exception {};

template
  template
T& Variant::get() &
{
  if (empty()) throw EmptyVariant();
  assert(is());
  return *this->template getBufferAs();
}

初始化

  • 从一个类型的值初始化一个Variant,通过一个VariantChoice构造函数实现,这个构造函数接受一个T类型的值
// variant/variantchoiceinit.hpp

template
VariantChoice::VariantChoice(const T& value)
{
  // getDerived()使用CRTP来操作派生类
  new(getDerived().getRawBuffer()) T(value); // 用placement new将value置入buffer
  // 设置派生类的discriminator为基类的Discriminator来表示存储的类型
  getDerived().setDiscriminator(Discriminator);
}

template
VariantChoice::VariantChoice(T&& value)
{
  new(getDerived().getRawBuffer()) T(std::move(value));
  getDerived().setDiscriminator(Discriminator);
}
  • 最终的目标是能从一个任意类型的值初始化Variant,甚至可以隐式转换
Variant v("hello"); // 隐式转换为string
  • 为此使用using声明把VariantChoice的构造函数引入到Variant中
using VariantChoice::VariantChoice...;
  • 这个using声明为Types中的每个类型生成一个Variant的构造函数
// Variant的构造函数实际是
Variant(const int&);
Variant(int&&);
Variant(const double&);
Variant(double&&);
Variant(const string&);
Variant(string&&);

析构

  • 当Variant初始化时,一个值构造到它的buffer中,destroy处理那个值的析构
// variant/variantchoicedestroy.hpp

template
bool VariantChoice::destroy()
{
  if (getDerived().getDiscriminator() == Discriminator)
  {
    // 如果匹配则调用placement delete
    getDerived().template getBufferAs()->~T();
    return true;
  }
  return false;
}
  • 只有当discriminator匹配时,VariantChoice::destroy()操作是有用的。但通常希望不考虑当前活动的类型,直接销毁存储在Variant中的值,因此Variant::destroy()将调用基类中所有的VariantChoice::destroy()
// variant/variantdestroy.hpp

template
void Variant::destroy()
{
  // 调用所有的VariantChoice::destroy(),至多一个成功
  bool results[] = { VariantChoice::destroy()...};
  this->setDiscriminator(0); // 表示不再存储一个值,Variant为空
}
  • 数组results的作用是提供一个能使用初始化列表的上下文,C++17中可以使用折叠表达式来消除对数组results的需要
// variant/variantdestroy17.hpp

template
void Variant::destroy()
{
  (VariantChoice::destroy(), ...);
  this->setDiscriminator(0);
}

赋值

  • 对于同类型直接赋值,对于不同类型则需要析构现有值,再用placement new重新初始化新类型的值
// variant/variantchoiceassign.hpp

template
auto VariantChoice::operator=(const T& value) -> Derived&
{
  if (getDerived().getDiscriminator() == Discriminator) // 同类型的值
  {
    *getDerived().template getBufferAs() = value;
  }
  else // 不同类型的值
  {
    getDerived().destroy(); // 用Variant::destroy()析构现有值
    new(getDerived().getRawBuffer()) T(value); // 用placement new初始化T类型新值
    getDerived().setDiscriminator(Discriminator);
  }
  return getDerived();
}

template
auto VariantChoice::operator=(T&& value) -> Derived&
{
  if (getDerived().getDiscriminator() == Discriminator) // 同类型的值
  {
    *getDerived().template getBufferAs() = std::move(value);
  }
  else // 不同类型的值
  {
    getDerived().destroy();
    new(getDerived().getRawBuffer()) T(std::move(value));
    getDerived().setDiscriminator(Discriminator);
  }
  return getDerived();
}
  • 和初始化一样,可以在Variant中使用using声明来继承赋值操作
using VariantChoice::operator=...;
  • 对于不同类型,赋值有两步,先析构再初始化,这将有三个需要考虑的问题:自赋值、异常、std::launder
  • 自赋值发生情况如下。如果使用两步赋值,源值将在拷贝前会被析构,导致内存崩溃。但好在自赋值意味着discriminator匹配,所以会使用同类型赋值,因此不会出问题
v = v.get()
  • 如果现有值的销毁已经完成,但是新值的初始化抛出异常,Variant::destroy()将discriminator值重置为0,表示Variant未存储值
// variant/variantexception.cpp

class CopiedNonCopyable : public std::exception {};

class NonCopyable {
 public:
  NonCopyable() = default;

  NonCopyable(const NonCopyable&)
  {
    throw CopiedNonCopyable();
  }

  NonCopyable(NonCopyable&&) = default;

  NonCopyable& operator=(const NonCopyable&)
  {
    throw CopiedNonCopyable();
  }

  NonCopyable& operator=(NonCopyable&&) = default;
};

int main()
{
  Variant v(42);
  try
  {
    NonCopyable nc;
    v = nc;
  }
  catch (CopiedNonCopyable)
  {
    std::cout << "Copy assignment of NonCopyable failed." << '\n';
    if (!v.is() && !v.is())
    {
      std::cout << "Variant has no value.";
    }
  }
}

// 程序输出为
Copy assignment of NonCopyable failed.
Variant has no value.
  • 访问一个没有值的variant将抛出EmptyVariant异常,以允许程序从这个异常条件修复
  • empty()成员函数用于检查Variant是否为空
// variant/variantempty.hpp

template
bool Variant::empty() const
{
  return this->getDiscriminator() == 0;
}
  • 第三个问题是标准化委员会在C++ 17标准化过程结束时才意识到的一个微妙问题。编译器通常希望产生高性能代码,而提高生成代码性能的主要机制是避免从内存到寄存器的重复的数据复制。为此编译器必须做一些假设,其中一种假设是某些类型的数据在其生命周期内是不可变的,这包括const数据、引用(可以初始化,但之后不修改)以及存储在多态对象中的一些bookkeeping data(用于调度虚拟函数、定位虚拟基类、以及处理typeid和dynamic_cast操作符)
  • 上面两步分配过程的问题是,它以一种编译器可能无法识别的方式悄悄地结束一个对象的生命周期,并在同一位置开始另一个对象的生命周期。因此,编译器可能假定它从Variant对象的先前状态获取的值仍然有效,而实际上placement new的初始化会使其无效,这就导致编译具有不可变数据成员的Variant时偶尔会产生无效结果。这种bug通常很难追踪(一部分原因是它们很少发生,另一部分原因是它们在源代码中并不真正可见)
  • C++17中,这个问题的解决方案是使用std::launder,它返回实参所表示地址的对象的指针,使得编译器不再假设原有值有效,从而起到修复的效果
  • 还有一些方法可以做得更好,比如额外添加一个指向buffer的指针成员,并在每次使用placement new赋值一个新值时获得已清洗的地址,但这些方法会使代码变得复杂,难以维护

访问者(Visitor)

  • 检查一个Variant中的所有可能类型会造成一个冗长的if语句链
if (v.is())
{
  std::cout << v.get();
}
else if (v.is())
{
  std::cout << v.get();
}
else
{
  std::cout << v.get();
}
  • 为此需要引入一个函数模板
template
void printImpl(const V& v)
{
  if (v.template is())
  {
    std::cout << v.template get();
  }
  else if constexpr (sizeof...(Tail) > 0)
  {
    printImpl(v);
  }
}

template
void print(const Variant& v)
{
  printImpl, Types...>(v);
}

int main()
{
  Variant v(3.14);
  print(v);
}
  • 但对于一个相对简单的操作来说,这是一个相当大的代码量。为了简化这点,可以用visit()来扩展Variant,它接受一个仿函数或者lambda,接受的参数就相当于访问者的角色
v.visit([] (const auto& value) { std::cout << value; });
  • variantVisitImpl()遍历Variant的类型,检查活动值是否具有给定类型,然后在找到适当类型时进行操作
template
R variantVisitImpl(V&& variant, Visitor&& vis, Typelist) {
  if (variant.template is())
  {
    return static_cast(
      std::forward(vis)(
        std::forward(variant).template get()));
  }
  else if constexpr (sizeof...(Tail) > 0)
  {
    return variantVisitImpl(std::forward(variant),
      std::forward(vis),
      Typelist());
  }
  else
  {
    throw EmptyVariant();
  }
}
  • visit()的实现直接委托给variantVisitImpl()即可,传递Variant本身,转发访问者,并提供完整的类型列表。三种实现分别在this对象作为Variant&、const Variant&或Variant&&传递时被调用,类成员函数后加引用限定符的用法可参考《Effective Modern C++》条款12
// variant/variantvisit.hpp

template
  template
VisitResult
Variant::visit(Visitor&& vis)& {
  using Result = VisitResult;
  return variantVisitImpl(*this, std::forward(vis),
    Typelist());
}

template
  template
VisitResult
Variant::visit(Visitor&& vis) const& {
  using Result = VisitResult;
  return variantVisitImpl(*this, std::forward(vis),
    Typelist());
}

template
  template
VisitResult
Variant::visit(Visitor&& vis) && {
  using Result = VisitResult;
  return variantVisitImpl(std::move(*this),
    std::forward(vis),
    Typelist());
}
  • 这里还需要实现VisitResult元函数。visit()的结果类型仍不能确定,比如接受如下lambda,其返回类型取决于输入的参数类型
[] (const auto& value) { return value + 1; }
  • visit()设置一个模板参数R,其默认实参为ComputedResultType
template
VisitResult visit(Visitor&& vis) &;
  • 现在希望显式指定返回结果的类型时,将R设为返回类型
// visit的返回类型是Variant中的类型
v.visit>([] (const auto& value) { return value + 1; });
  • 不显式指定时,将使用R的默认实参ComputedResultType(一个只需声明无需实现的类,只是用作标签)
class ComputedResultType;
  • 元函数VisitResult的实现如下
// 显式指定返回类型的情形
template
class VisitResultT {
 public:
  using Type = R;
};

// 对使用默认实参ComputedResultType的情形特化一个版本
template
class VisitResultT {
  ... // 实现见后
}

template
using VisitResult = typename VisitResultT::Type;
  • 对于通用类型(common type),C++已经有了一个合理的概念:在三元表达式b ? x : y中,结果类型是x和y之间的通用类型,例如x为int,y为double,则通用类型为double。由此实现CommonType如下
using std::declval;

template
class CommonTypeT {
 public:
  using Type = decltype(true? declval() : declval());
};

template
using CommonType = typename CommonTypeT::Type;
  • 特化的VisitResultT得到对Variant的每个类型调用访问者时生成的结果类型的通用类型,实现如下
// 对T类型值调用访问者时生成的结果类型
template
using VisitElementResult = decltype(declval()(declval()));

// 对每个元素类型调用访问者时的通用结果类型
template
class VisitResultT {
  using ResultTypes = // 每个元素调用访问者时生成的结果类型
    Typelist...>;
 public:
  using Type = // 所有结果类型的通用类型
    Accumulate, CommonTypeT, Front>;
};
  • 标准库提供了std::common_type,它组合了CommonTypeT和Accumulate以对任意数量类型生成通用类型,因此特化的VisitResultT可以更简单地实现如下
template
class VisitResultT {
 public:
  using Type =
    std::common_type_t...>;
};
  • 下例说明了visit()接受一个访问者时的返回类型
Variant v(3.14);
auto result = v.visit([] (const auto& value) { return value + 1; });
std::cout << typeid(result).name(); // double

默认构造

  • Variant应该允许默认构造,否则每次使用都要指定初始值。对于默认构造的方式,可能会想到设置discriminator为0来表示没有存储值,但这样的空Variant不能被访问或找到任何要提取的值,并且将空Variant的异常状态提升到了通用状态
  • 一个避免引入空Variant的方法是初始化类型列表中首个类型的一个值
// variant/variantdefaultctor.hpp

template
Variant::Variant()
{
  *this = Front>();
}
  • 这个方法是简单和可预测的
Variant v;
if (v.is()) std::cout << v.get() <<'\n'; // 0(int类型)
Variant v2;
if (v2.is()) std::cout << v2.get(); // 0(double类型)

拷贝和移动构造

  • 要拷贝一个Variant需要确定它当前存储的类型,将该值拷贝构造到字符数组中,并设置discriminator,这两点在VariantChoice的拷贝赋值运算符中已提供,通过对要拷贝的Variant使用visit()即可简洁紧凑地实现目的
// variant/variantcopyctor.hpp

template
Variant::Variant(const Variant& source)
{
  if (!source.empty())
  {
    source.visit([&] (const auto& value) {
      *this = value; // 从VariantChoice继承的拷贝赋值运算符
    });
  }
}
  • 移动构造同理
// variant/variantmovector.hpp

template
Variant::Variant(Variant&& source)
{
  if (!source.empty())
  {
    std::move(source).visit([&] (auto&& value) { *this = std::move(value); });
  }
}
  • 基于visit的实现也适用于模板化形式的拷贝和移动构造
// variant/variantcopyctortmpl.hpp

template
  template
Variant::Variant(const Variant& source)
{
  if (!source.empty())
  {
    source.visit([&] (const auto& value) { *this = value; });
  }
}


// variant/variantmovectortmpl.hpp

template
  template
Variant::Variant(Variant&& source)
{
  if (!source.empty())
  {
    std::move(source).visit([&](auto&& value) { *this = std::move(value); });
  }
}
  • 下面是Variant的构造和赋值的示例
Variant v1((short)42);
Variant v2(v1); // short到int的整型提升
std::cout << v2.get(); // 42(int类型)

v1 = 3.14f;
Variant v3(std::move(v1)); // float到double的浮点提升
std::cout << v3.get(); // 3.14(double类型)

v1 = "hello";
Variant v4(std::move(v1)); // const char*到std::string的转换
std::cout << v4.get(); // hello

赋值运算符

  • 赋值运算符与拷贝和移动构造类似
// variant/variantcopyassign.hpp

template
Variant& Variant::operator=(const Variant& source)
{
  if (!source.empty())
  {
    source.visit([&] (const auto& value) { *this = value; });
  }
  else
  {
    destroy(); // 源Variant为空时析构目标,隐式设置discriminator为0
  }
  return *this;
}


// variant/variantmoveassign.hpp

template
Variant& Variant::operator= (Variant&& source)
{
  if (!source.empty())
  {
    std::move(source).visit([&] (auto&& value) { *this = std::move(value); });
  }
  else
  {
    destroy();
  }
  return *this;
}


// variant/variantcopyassigntmpl.hpp

template
  template
Variant& Variant::operator=(const Variant& source)
{
  if (!source.empty())
  {
    source.visit([&] (const auto& value) { *this = value; });
  }
  else
  {
    destroy();
  }
  return *this;
}


// variant/variantmoveassigntmpl.hpp

template
  template
Variant& Variant::operator=(Variant&& source)
{
  if (!source.empty())
  {
    std::move(source).visit([&] (auto&& value) { *this = std::move(value); });
  }
  else
  {
    destroy();
  }
  return *this;
}

你可能感兴趣的:(23 标签联合)