发表于2012年12月13日,作者 Andrzej Krzemieński

我假定你已经很熟悉常量表达式函数了(如果不熟悉的话,可以看看这个简短的介绍)。在这篇文章中,我会分享一些在常量表达式中使用联合体的经验。联合体现在并不太流行,因为它有很大的类型安全漏洞,但是我发现当我们使用Fernando Cacciola 的onstd::optional 提案的时候,联合体却具有无可估量的价值。

背景

你知道Boost.Optional库吗?简单的说,boost::optional 是一个复合值,它可以存储T类型的任何值和一个额外的状态量,用来指出是否有T值被存储,它是一个可以被置空的T。

boost::optional的特征之一是:当你把它初始化来存储空状态的时候,它根本不会构造任何T类型变量,它甚至不会调用T的缺省构造函数——因为性能原因,同时也可以防止T根本没有缺省构造函数。一种实现这个的方法是提供一些足够大的原始内存缓冲区来存储任意类型的T,仅当需要的时候,使用原状构造函数(replacement constructor)去初始化它:

template <typename T>
class optional
{
  bool initialized_;
  char storage_[ sizeof(T) ];
  // ...
};

以上代码仅给你了一个可能的实现方法。在实际应用中,由于内存对齐问题,这种方式不一定奏效;我们可能不得不用std::aligned_storage;或者一个判断析取(discriminated union)——这个在Cassio Neri在ACCU Overload #112上的文章可以看到详细描述。空状态的构造函数和“存在值”的构造函数可以这么实现:

optional<T>::optional(none_t)  // null-state tag
{
  initialized_ = false;
  // leave storage_ uninitialized
};

optional<T>::optional(T const& val)
{
  new (storage_) T{val};       // placement new
  initialized_ = true;
};

析构函数的定义,正如上面所讲的,会和下面这段代码相似:

optional<T>::~optional()
{
  if (initialized_) {
    static_cast<T*>(storage_) -> T::~T(); 
  }
};

第一个问题

如果有std::optional,那么在标准库中就会有其他特征会用到这个库。其中一个特征就是std::optional将会是一种文本类型:一种可在编译器可以被当成常量来用的类型。这种用法的一个约束是,标准库会强行要求它提供一个无意义的析构函数:这个析构函数什么也不做。从以上我们可以看到,optional的析构函数必须做一些事情,因此,通常而言我们无法实现这个目标。然而,我们可以通过无意义析构函数本身来实现这个目标。想象下T是int类型:我们并不是一定要调用int的析构函数——因为这没有意义。在这里,我们可以实现一种实际意义上的无意义析构函数:这种析构函数可以被跳过(仅仅是不调用),而不产生任何坏处。

对文本类型的另一个要求是,它们必须至少有一个常量构造函数,此构造函数被用来创建编译期常量。然而,为了避免“编译器未定义行为”,标准库给常量构造函数和常量类型强加了很多约束,来确保没有数据成员或者基类对象数据未被初始化。因此,我们用适当长度的char数组来实现optional的方法并不会奏效,因为在“值”构造函数里,数组并不在初始化列表里。我们可以把数组在空状态构造函数里用0来填充,但是这仍然会产生运行期间的开销。如果我们用std::aligned_storage来实现,会有相似的问题存在。我们也不能用一个简单的判断析取来实现(参考ACCU Overload #112):

template <typename T>
class optional
{
  bool initialized_;
  union { T storage_ }; // anonymous union
};

为了创建一个“未初始化的”optional对象,我们不得不让匿名联合体保持未初始化,但这和常量函数或者默认初始化存储不兼容,这和我们的设计目标是背道相驰:我们希望跳过T类型的默认构造函数。

第二个问题

另一个明显的设计目标是能生成产生optional对象的值的函数的属性。在 boost::optional和std::optional提案中,为了访问对象中包含的值,我们使用*操作符(取值操作符)。为了保证运行期间的最好性能,我们不需要检查是否optional对象是用一个有意义的值来初始化的:我们把这个要求定义为函数的前置条件,它会隐式的访问存储T的空间:

explicit optional<T>::operator bool() const // check for being initialized
{
  return initialized_;
};

T const& optional<T>::operator*() const
// precondition: bool(*this)
{
  return *static_cast<T*>(storage_);
}

同时,我们想要在编译期调用*操作符,在这种情况下,如果我们尝试访问一个未被任何值初始化的optional对象,我们希望编译会失败。当然,我们也可以貌相使用我另一篇关于编译期计算的文章中提及的方法。

constexpr explicit optional<T>::operator bool()
{
  return initialized_;
};

constexpr T const& optional<T>::operator*()
{
  return bool(*this) ? *static_cast<T*>(storage_) : throw uninitialized_optional();
}

然而,这并不可行。我们可以实现想要的编译期行为,但是我们可能必须强制执行一个可以破坏性能的运行期检查。那么,我们可不可以在编译期做这个检查,而不是运行期?

解决方案

以上的问题都可以通过使用T的联合体,和一个仿类、空类来解决:

struct dummy_t{};

template <typename T>

union optional_storage
{
  static_assert( is_trivially_destructible<T>::value, "" );

  dummy_t dummy_;
  T       value_;

  constexpr optional_storage()            // null-state ctor
    : dummy_{} {}

  constexpr optional_storage(T const& v)  // value ctor
    : value_{v} {}

  ~optional_storage() = default;          // trivial dtor
};

在常量函数和构造函数中国使用联合体有一些特殊的规则。我们只能初始化联合体中的一个成员。(很明显你不可能初始化超过一个成员,因为他们使用同一个存储空间。)这个成员被称为被激活成员。假如我们想让存储空间处于未初始化状态,我们可以初始化伪成员。这符合编译期初始化的所有常规需求,由于dummy_t并不包含任何数据,因此它不会在运行期产生任何开销。

第二,读取(严格的说,是一个触发左值到右值的转换过程)联合体中未激活成员并未被定义成常量表达式的核心部分。在编译期计算中使用它会产生编译错误,以下的例子说明了这种情况:

constexpr optional_storage<int> oi{1}; // ok
constexpr int i = oi.value_;           // ok
static_assert(i == 1, "");             // ok

constexpr optional_storage<int> oj{};  // ok
constexpr int j = oj.value_;           // compile-time error

optional类模板(针对无意义的可被析构的T对象),可以用如下方法实现:

template <typename T>
// requires: is_trivially_destructible<T>::value
class optional
{
  bool initialized_;
  optional_storage<T> storage_;

public:
  constexpr optional(none_t) : initialized_{false}, storage_{} {}

  constexpr optional(T const& v) : initialized_{true}, storage_{v} {}

  constexpr T const& operator*() 
  // precondition: bool(*this) 
  {
    return storage_.value_;
  }

  // ...
};

*操作符的编译器错误信息并不是很理想:它并没有提及optional对象没有被初始化,而只提到了未激活联合体成员的用法。但是,最主要的目的实现了:我们成功阻止了程序在编译期访问无效值。

你可以在std::optional提案中找到参考的实现方法。

原文:Constexpr unions