Boost C++ 库

...世界上最受推崇和专业设计的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu, C++ 编码标准

PrevUpHomeNext

值初始化

简介
详情
类型和对象
参考文献
参考
致谢

在 C++ 中以通用方式构造和初始化对象是很困难的。问题在于初始化有几个不同的规则适用。根据类型,新构造对象的值可以是零初始化(逻辑 0)、默认构造(使用默认构造函数)或不确定的。在编写通用代码时,必须解决此问题。模板 value_initialized 提供了一种解决方案,它为标量、联合和类类型的值初始化提供了统一的语法。此外,value_initialized 还为解决关于值初始化的各种编译器问题提供了变通方法。

此外,还提供了一个 const 对象 initialized_value,以避免从 value_initialized<T> 对象检索值时重复类型名称。

在 C++ 中,有多种方法可以初始化变量。以下声明都可能将局部变量初始化为其默认值

T1 var1;
T2 var2 = 0;
T3 var3 = {};
T4 var4 = T4();

不幸的是,这些声明是否正确初始化变量很大程度上取决于其类型。根据定义,第一个声明对于任何 默认可构造 类型都是有效的。

但是,它并非总是执行初始化。当它是类的实例,并且类的作者提供了适当的默认构造函数时,它会正确地初始化变量。另一方面,当 var1 的类型是算术类型(如 intfloatchar)时,其值是不确定的

算术变量当然可以通过第二个声明 T2 var2 = 0 正确初始化。但是,除非该类经过专门编写以支持以这种方式初始化,否则此初始化形式通常不适用于类类型。

第三种形式 T3 var3 = {} 初始化聚合,通常是“C 风格”的 struct 或“C 风格”的数组。但是,在本库开发时,该语法不允许具有显式声明构造函数的类。

第四种形式是其中最通用的形式,因为它可以用于初始化算术类型、类类型、聚合、指针和其他类型。声明 T4 var4 = T4() 应理解如下:首先,通过 T4() 创建一个临时对象。此对象是值初始化的。接下来,临时对象被复制到命名变量 var4。之后,临时对象被销毁。虽然复制和销毁很可能会被优化掉,但 C++ 仍然要求类型 T4可复制构造 的。因此,T4 需要同时默认可构造可复制构造

类可能不是可复制构造的,例如,因为它可能具有私有且未定义的复制构造函数,或者因为它可能派生自 boost::noncopyable。Scott Meyers [2] 解释了为什么一个类会像那样定义。

第四种形式 T4 var4 = T4() 还有另一个不太明显的缺点:它受到各种 编译器问题 的影响,导致变量在某些编译器特定的情况下保持未初始化状态。

模板 value_initialized 提供了一种通用的对象初始化方法,例如 T4 var4 = T4(),但不需要其类型是 可复制构造 的。它还为解决关于值初始化的那些编译器问题提供了变通方法。它允许获取任何类型的初始化变量;它要求类型是 默认可构造 的。类型为 T 的正确值初始化对象通过以下声明构造

value_initialized<T> var;

模板 initialized 同时提供值初始化和直接初始化。它作为数据成员类型特别有用,允许相同的对象进行直接初始化或值初始化。

const 对象 initialized_value 允许按如下方式值初始化变量

T var = initialized_value;

这种初始化形式在语义上等同于 T4 var4 = T4(),但可以避免上述编译器问题。

C++ 标准 [3] 包含 -初始化默认-初始化 的定义。非正式地,零初始化意味着给对象赋予初始值 0(转换为类型),而默认初始化意味着 POD [4] 类型是零初始化的,而非 POD 类类型则使用其相应的默认构造函数进行初始化。

声明可以包含初始化器,它指定对象的初始值。初始化器可以只是 '()',它表示对象应进行值初始化(但请参见下文)。但是,如果声明没有初始化器,并且它是非 const、非 static POD 类型,则初始值是不确定的:(有关准确的定义,请参见 §8.5, [dcl.init])。

int x; // no initializer. x value is indeterminate.
std::string s; // no initializer, s is default-constructed.

int y = int();
// y is initialized using copy-initialization
// but the temporary uses an empty set of parentheses as the initializer,
// so it is default-constructed.
// A default constructed POD type is zero-initialized,
// therefore, y == 0.

void foo ( std::string ) ;
foo ( std::string() ) ;
// the temporary string is default constructed
// as indicated by the initializer ()
值初始化

C++ 标准的第一个技术勘误 (TC1) 的草案于 2001 年 11 月公开发布,其中引入了 核心问题 178 以及许多其他问题。

该问题引入了 -初始化 的新概念,并修正了零初始化的措辞。非正式地,值初始化类似于默认初始化,但例外情况是在某些情况下,非静态数据成员和基类子对象也进行值初始化。

不同之处在于,值初始化的对象不会具有,或者至少不太可能具有数据成员和基类子对象的不确定值;这与默认构造的对象的情况不同(有关规范性描述,请参见核心问题 178)。

为了指定对象的值初始化,我们需要使用空集初始化器:()

与之前一样,没有初始化器的声明指定默认初始化,而具有非空初始化器的声明指定复制(=xxx)或直接(xxx)初始化。

template<class T> void eat(T);

int x ; // indeterminate initial value.

std::string s; // default-initialized.

eat ( int() ) ; // value-initialized

eat ( std::string() ) ; // value-initialized
值初始化语法

值初始化使用 () 指定。但是,初始化器的语法不允许使用空括号集,因为它被解析为不带参数的函数的声明

int x() ; // declares function int(*)()

因此,空的 () 必须放在其他初始化上下文中。

一种替代方法是使用复制初始化语法

int x = int();

这对于 POD 类型非常有效。但是对于非 POD 类类型,复制初始化会搜索合适的构造函数,例如,复制构造函数。它还会搜索合适的转换序列,但这在此上下文中不适用。

对于任意未知类型,使用此语法可能没有预期的值初始化效果,因为我们不知道从默认构造的对象复制是否与默认构造的对象完全相同,并且编译器在某些情况下(但从不要求)允许优化掉复制。

一种可能的通用解决方案是使用非静态数据成员的值初始化

template<class T>
struct W
{
    // value-initialization of 'data' here.
    W() : data() {}

    T data;
};

W<int> w;
// w.data is value-initialized for any type.

这是早期版本的 value_initialized<T> 模板类提供的解决方案。不幸的是,这种方法受到各种编译器问题的困扰。

编译器问题

各种编译器尚未完全实现值初始化。因此,当根据 C++ 标准应该值初始化对象时,由于这些编译器问题,实际上可能仍然保持未初始化状态。很难对这些问题是什么样子做出一般性陈述,因为它们取决于您正在使用的编译器、其版本号以及您想要值初始化的对象类型。

到目前为止,我们测试过的所有编译器都正确地支持算术类型的值初始化。但是,当应该值初始化时,各种编译器可能会使某些类型的聚合保持未初始化状态。在各种编译器上,成员指针类型对象的值初始化也可能出错。

在撰写本文时(2010 年 5 月),以下关于值初始化的报告问题仍然存在于当前的编译器版本中

请注意,关于值初始化的所有已知 GCC 问题都已在 GCC 版本 4.4 中修复,包括 GCC 错误 30111。据我们所知,Clang 也已完全实现了值初始化,现在 Clang 错误 7139 已修复。

value_initialized 的新版本(Boost 发布版本 1.35 或更高版本)为这些问题提供了变通方法:value_initialized 现在可能会清除其内部数据,然后再构造其包含的对象。对于那些需要这种变通方法的编译器,它将基于 编译器缺陷宏 BOOST_NO_COMPLETE_VALUE_INITIALIZATION 执行此操作。

namespace boost {

template<class T>
class value_initialized
{

  public :

    value_initialized() : x() {}

    operator T const &() const { return x ; }

    operator T&() { return x ; }

    T const &data() const { return x ; }

    T& data() { return x ; }

    void swap( value_initialized& );

  private :

    [unspecified] x ;

} ;

template<class T>

T const& get ( value_initialized<T> const& x )
{
  return x.data();
}

template<class T>
T& get ( value_initialized<T>& x )
{
  return x.data();
}

template<class T>
void swap ( value_initialized<T>& lhs, value_initialized<T>& rhs )
{
  lhs.swap(rhs);
}

} // namespace boost

此模板类的对象是一个可转换为 'T&'T 包装器,其包装对象(T 类型的数据成员)在此包装器类的默认初始化时进行值初始化

int zero = 0;
value_initialized<int> x;
assert( x == zero ) ;

std::string def;
value_initialized< std::string > y;
assert( y == def ) ;

此包装器的目的是为标量、联合和类类型(POD 和非 POD)的值初始化提供一致的语法,因为值初始化的正确语法各不相同(请参见 值初始化语法)。

可以通过转换运算符 T&、成员函数 data() 或非成员函数 get() 访问包装的对象

void watch(int);

value_initialized<int> x;

watch(x) ; // operator T& used.
watch(x.data());
watch( get(x) ) // function get() used

可以包装 const 和非 const 对象。可变对象可以直接从包装器内部修改,但常量对象不能

T可交换 类型时,value_initialized<T> 也是可交换的,可以通过调用其 swap 成员函数以及调用 boost::swap 来实现。

value_initialized<int> x;
static_cast<int&>(x) = 1 ; // OK
get(x) = 1 ; // OK

value_initialized<int const> y ;
static_cast<int&>(y) = 1 ; // ERROR: cannot cast to int&
static_cast<int const&>(y) = 1 ; // ERROR: cannot modify a const value
get(y) = 1 ; // ERROR: cannot modify a const value
[Warning] 警告

Boost 版本 1.40.0 及更早版本的 value_initialized 实现允许通过其转换运算符和 data() 成员函数从常量包装器非 const访问包装的对象。

例如

value_initialized<int> const x_c;
int& xr = x_c ; // OK, conversion to int& available even though x_c is itself const.
xr = 2 ;

这种晦涩行为的原因是某些编译器不接受以下有效代码

struct X
{
  operator int&() ;
    operator int const&() const ;
  };
  X x ;
  (x == 1) ; // ERROR HERE!

当前版本的 value_initialized 不再具有这种晦涩的行为。由于现在的编译器广泛支持通过具有 constnon-const 版本来重载转换运算符,因此我们已决定相应地解决此问题。因此,当前版本支持逻辑常量性的概念。

推荐实践:非成员 get() 惯用法

如果要避免能够从常量包装器内部修改非 const 包装对象(如先前版本的 value_initialized 支持的那样)的晦涩行为,则始终应使用 get() 惯用法来访问包装的对象

value_initialized<int> x;
get(x) = 1; // OK
value_initialized<int const> cx;
get(x) = 1; // ERROR: Cannot modify a const object

value_initialized<int> const x_c;
get(x_c) = 1; // ERROR: Cannot modify a const object

value_initialized<int const> const cx_c;
get(cx_c) = 1; // ERROR: Cannot modify a const object
namespace boost {

template<class T>
class initialized
{

  public :

    initialized() : x() {}

    explicit initialized(T const & arg) : x(arg) {}

    operator T const &() const;

    operator T&();

    T const &data() const;

    T& data();

    void swap( initialized& );

  private :

    [unspecified] x ;

};

template<class T>
T const& get ( initialized<T> const& x );

template<class T>
T& get ( initialized<T>& x );

template<class T>
void swap ( initialized<T>& lhs, initialized<T>& rhs );

} // namespace boost

模板类 boost::initialized<T> 同时支持值初始化和直接初始化,因此其接口是 value_initialized<T> 接口的超集:其默认构造函数值初始化包装的对象,就像 value_initialized<T> 的默认构造函数一样,但 boost::initialized<T> 还提供了一个额外的 explicit 构造函数,该构造函数通过指定的值直接初始化包装的对象。

initialized<T> 在包装的对象必须根据运行时条件进行值初始化或直接初始化时特别有用。例如,initialized<T> 可以保存数据成员的值,该值可以通过某些构造函数进行值初始化,而可以通过其他构造函数进行直接初始化。

另一方面,如果事先知道对象始终必须进行值初始化,则 value_initialized<T> 可能是更可取的。如果对象始终必须进行直接初始化,则实际上不需要使用这两个包装器中的任何一个。

namespace boost {
class initialized_value_t
{
  public :
    template <class T> operator T() const ;
};

initialized_value_t const initialized_value = {} ;

} // namespace boost

initialized_value 提供了一种获取初始化值的便捷方法:其转换运算符为任何 可复制构造 类型提供适当的值初始化对象。

假设您需要一个类型为 T 的初始化变量。您可以这样做,如下所示

T var = T();

但是如前所述,这种形式受到各种编译器问题的困扰。模板 value_initialized 提供了一种变通方法

T var = get( value_initialized<T>() );

不幸的是,两种形式都重复了类型名称,现在类型名称相当短 (T),但当然可能更像 Namespace::Template<Arg>::Type

相反,可以使用 initialized_value,如下所示

T var = initialized_value;
  1. Bjarne Stroustrup、Gabriel Dos Reis 和 J. Stephen Adamczyk 撰写了多篇论文,建议扩展对 C++ 中花括号括起来的初始化列表的支持。自 C++11 以来,此功能现已可用。这将允许通过执行 T var = {} 对任何 默认可构造 类型 T 的变量 var 进行值初始化。这些论文列在 Bjarne 的网页 My C++ Standards committee papers 上。
  2. Scott Meyers,《Effective C++》,第三版,第 6 项,显式禁止使用您不需要的编译器生成函数Scott Meyers: Books and CDs
  3. 《C++ 标准》,第二版 (2003),ISO/IEC 14882:2003
  4. POD 代表 “普通旧数据”

参考

namespace boost {
  template<typename T> class initialized;
  class initialized_value_t;
  template<typename T> class value_initialized;

  initialized_value_t const initialized_value;
  template<typename T> T const & get(initialized< T > const & x);
  template<typename T> T & get(initialized< T > & x);
  template<typename T> 
    void swap(initialized< T > & lhs, initialized< T > & rhs);
  template<typename T> T const & get(value_initialized< T > const & x);
  template<typename T> T & get(value_initialized< T > & x);
  template<typename T> 
    void swap(value_initialized< T > & lhs, value_initialized< T > & rhs);
}

value_initialized 由 Fernando Cacciola 开发,并得到了 David Abrahams 和 Darin Adler 的帮助和建议。

特别感谢 Bjorn Karlsson 仔细编辑并完成了本文档。

为了解决各种编译器问题,Fernando Cacciola 和 Niels Dekker 为 Boost 发布版本 1.35 (2008) 重新实现了 value_initialized

boost::initialized 的灵感很大程度上来自 Edward Diener 和 Jeffrey Hellrung 的反馈。

initialized_value 由 Niels Dekker 编写,并添加到 Boost 发布版本 1.36 (2008)。

Fernando Cacciola 开发。此文件的最新版本可在 www.boost.org 上找到。


PrevUpHomeNext