在 C++ 中,以通用的方式构造和初始化对象是很困难的。问题在于存在几种不同的初始化规则。根据类型不同,新构造的对象的值可以是零初始化(逻辑上为 0)、默认构造(使用默认构造函数)或不确定值。编写泛型代码时,必须解决这个问题。模板 value_initialized
提供了一种解决方案,它为标量、联合体和类类型的值初始化提供了统一的语法。此外,value_initialized
提供了解决各种编译器关于值初始化问题的变通方法。
此外,还提供了一个 const
对象 initialized_value
,以便在从
对象检索值时避免重复类型名称。value_initialized
<T>
在 C++ 中,有多种初始化变量的方法。以下声明都可能会将局部变量初始化为其默认值
T1 var1; T2 var2 = 0; T3 var3 = {}; T4 var4 = T4();
不幸的是,这些声明是否正确地初始化变量很大程度上取决于其类型。第一个声明根据定义对任何可默认构造类型都是有效的。
但是,它并不总是执行初始化。当它是类的实例并且类的作者提供了正确的默认构造函数时,它会正确地初始化变量。另一方面,当 var1
的类型是算术类型(如 int
、float
或 char
)时,其值是不确定的。
当然,算术变量可以通过第二个声明 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 Bug 30111。据我们所知,自Clang Bug 7139修复以来,Clang 也已完全实现值初始化。
新版本的value_initialized
(Boost 1.35 或更高版本)提供了一种解决这些问题的变通方法:对于需要这种变通方法的编译器,value_initialized
现在可以在构造它包含的对象之前清除其内部数据。这将基于编译器缺陷宏BOOST_NO_COMPLETE_VALUE_INITIALIZATION
来进行。
namespace boost { template<class T> classvalue_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 ; // OKvalue_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
警告 | |
---|---|
Boost 1.40.0 及更早版本中的 例如
这种模糊行为的原因是某些编译器不接受以下有效代码。 struct X { operator int&() ; operator int const&() const ; }; X x ; (x == 1) ; // ERROR HERE! 当前版本的 |
如果始终使用get()
惯用法访问包装的对象,则可以避免从常量包装器中修改非const
包装对象(如早期版本的value_initialized
所支持的)的模糊行为。
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> classinitialized
{ public :initialized
() : x() {} explicitinitialized
(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 { classinitialized_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
;
T
的变量var
通过T var = {}
进行值初始化。这些论文列在Bjarne的网页上,我的C++标准委员会论文。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仔细编辑并完成了这份文档。
value_initialized
由Fernando Cacciola和Niels Dekker为Boost 1.35版(2008)重新实现,提供了解决各种编译器问题的方案。
boost::
很大程度上受到了Edward Diener和Jeffrey Hellrung反馈的启发。initialized
initialized_value
由Niels Dekker编写,并添加到Boost 1.36版(2008)。
由Fernando Cacciola开发。此文件的最新版本可在www.boost.org找到。