頭文件 <boost/utility/value_init.hpp>

目錄

原理
簡介
細節
類型和對像
鳴謝


原理

在C++中,以泛型的方式構造和初始化對象是困難的。問題在於初始化時有幾種不同的規則。取決於對象的類型,一個新構造的對象的值可以被零初始化(邏輯0)、缺省構造(使用缺省構造函數),或者不確定。在編寫泛型代碼時,問題必須被解決。模板 value_initialized 提供了一個解決方案,以一致的語法來對標量、聯合以及類類型進行值的初始化。此外,value_initialized 為各種編譯器對於值初始化所存在的問題提供了變通方法。此外,還提供了一個常量對像 initialized_value,以避免在從 value_initialized<T> 對像中取出值的時候重複類型名。

簡介

在C++中有多種方法來初始化一個變量。以下所有聲明都可以將一個局部變量初始化為它的缺省值:

  T1 var1;
T2 var2 = 0;
T3 var3 = {};
T4 var4 = T4();
不幸的是,這些聲明是否對變量進行了正確的初始化,非常依賴於變量的類型。第一個聲明對於任意 可缺省構造 的類型均有效(通過定義)。但是,它並不總是會進行初始化!如果該變量是一個類的實例,且該類的作者提供了適當的缺省構造函數,就會正確地初始化該變量。另一方面,如果變量的類型為算術類型如 int, float, 或 char,則 var1 的值是不確定的算術類型的變量當然可以通過第二個聲明 T2 var2 = 0 正確地進行初始化。不過這種初始化形式通過不能用於類類型(除非被特別編寫為支持這種初始化方式)。第三種形式,T3 var3 = {} 用於初始化一個聚合類型,典型例子如"C風格"的結構或"C風格"的數組。不過,這種語法又不能用於具有顯式聲明的構造函數的類(但是請留意即將到來的C++語言的變化,由 Bjarne Stroustrup 等提出[1]!)。第四種形式是幾種形式中最為通用的,它可以用於初始化算術類型、類類型、聚合類型、指針及其它類型。聲明 T4 var4 = T4(), 可以解讀如下:首先以 T4() 創建一個臨時對象。該對象是 value-initialized 的。接著該臨時對像被複製給命名變量,var4. 然後臨時對像被銷毀。雖然複製和析構操作都很可能被優化掉,但是C++仍然要求類型 T4可複製構造的。(因此 T4 需要既是 可缺省構造的 也是 可複製構造的)。一個類有可能不是可複製構造的,例如它可能具有私有且未定義的複製構造函數,或者它可能派生自 boost::noncopyable. Scott Meyers [2] 解釋了為什麼一個類會如此定義。

第四種形式 T4 var4 = T4() 有一個不太明顯的缺點:它會遭到各種 編譯器問題,在某些編譯器的特定情形下會導致變量未初始化。

模板 value_initialized 提供了一個通用的方法來初始化一個對象,如 T4 var4 = T4(), 但不要求其類型為可複製構造。而且它還為具有值初始化問題的編譯器提供了變通方法!它可以獲得任意類型的一個已初始化變量;它要求類型為可缺省構造。類型 T 的一個正確的 值已初始化 對像聲明如下:

  value_initialized<T> var;

常量對像 initialized_value 允許按以下方式對一個變量進行值初始化:

  T var = initialized_value ;

這種初始化的形式語義上等價於 T4 var4 = T4(), 但在應對前述的編譯器問題時更為健壯。

細節

C++標準[3]中包含了零初始化和缺省初始化的定義。 簡單地說,零初始化是指對像被賦予初始值 0 (轉換為該類型),缺省初始化則是指對 POD [4] 類型進行零初始化,而對類類型則用對應的缺省構造函數進行初始化。聲明中可以包含初始化器,其中指定對象的初始值。初始化器可以只是一個 '()', 這表示該對像被缺省初始化(見下文)。但是,如果聲明中沒有初始化器,而且它是一個非常量、非靜態的 POD類型,那麼初始值就是不確定的:(精確定義請見 §8.5)

int x ; // 無初始化器,x 的值不確定。
std::string s ; // 無初始化器,s 被缺省構造。
int y = int() ;
// y 使用複製初始化,臨時變量使用空括號作為初始化器,所以是缺省構造的。
// 缺省構造的 POD 類型是零初始化的,因此 y == 0.
void foo ( std::string ) ;
foo ( std::string() ) ;
// 臨時字符串是缺省構造的,正如初始化器 () 所表示的

value-initialization

第一份 Technical Corrigendum for the C++ Standard (TC1), 該草案在2001年9月公開發佈,提出了 Core Issue 178 (當然還有其它很多問題)。

該問題引入了"值初始化"的新概念(它也修正了零初始化的說法)。簡單地說,值初始化類似於缺省初始化,除了在有些情況 下,非靜態數據成員和基類子對象也可以進行值初始化。區別在於一個進行值初始化的對象,其數據成員或基類子對像不具有(至少不太可能具有) 不確定的值;而不是象缺省構造的對象那樣。(正式描述請見 Core Issue 178)

為了對一個對像指定值初始化,我們需要使用空初始化器:().

和以前一樣,不帶初始化器的聲明表示缺省初始化,帶有非空初始化器的聲明則表示複製(=xxx)或直接(xxx)初始化。

template<class T> void eat(T);
int x ; // 不確定的初始值
std::string s; // 缺省初始化
eat ( int() ) ; // 值初始化
eat ( std::string() ) ; // 值初始化

value-initialization 的語法

值初始化以 () 指定。但是空括號不允許作為初始化器的語法,因為它會被當作一個不帶參數的函數聲明:

int x() ; // 聲明一個函數 int(*)()
int y ( int() ) ; // 聲明一個函數 int(*)( int(*)() )

因此,空的 () 必須被放在其它初始化上下文中。

另一個方法是使用複製初始化語法:

int x = int() ;

這種方法對於 POD 類型是可以的。但是對於非-POD 的類類型,複製初始化會查找合適的構造函數,結果可能是複製構造函數(系統也會查找合適的轉換函數,不過在這個上下文中不適用)。對於任意的未知類型,使 用這一語法可能無法達到想要的值初始化的效果,因為我們不知道從一個缺省構造的對象進行複製是否可以得到與缺省構造的對象完全一樣的結果,而且編譯器被允 許(在某些情況下),但不是被要求,將複製操作優化掉。

一種可能的通用解決方法是使用非靜態數據成員的值初始化:

template<class T> 
struct W
{
// 'data' 的值初始化
W() : data() {}
T data ;
} ;
W<int> w ;
// 對於任何類型,w.data 都是值初始化的

這正是 value_initialized<T> 模板類所提供的解決方法。不幸的是,這種方法要遭到各種編譯器問題。

編譯器問題

各種編譯還不能完全地實現值初始化。因此當一個對像進行 值初始化 (根據C++標準)時,它實際上可能會由於這些編譯器的問題而保持為未初始化!很難對這些問題有一個綜合的描述,因為它們取決於你所使用的編譯器、版本號,以及你要進行值初始化的對象類型。編譯器通常可以正確支持內建類型的值初始化。但是用戶自定義類型的對象以及某些特定情況的聚合類型在進行值初始化時,可能會完全保持為未初始化。

我們已經在 Microsoft, Sun, Borland, 和 GNU 的編譯器上遇到了有關值初始化的問題。以下是這些問題的缺陷報告:
Microsoft Feedback ID 100744 - Value-initialization in new-expression
由 Pavel Kuznetsov (MetaCommunications Engineering) 報告, 2005-07-28
GCC Bug 30111 - Value-initialization of POD base class doesn't initialize members
由 Jonathan Wakely 報告, 2006-12-07
GCC Bug 33916 - Default constructor fails to initialize array members
由 Michael Elizabeth Chastain 報告, 2007-10-26
Borland Report 51854 - Value-initialization: POD struct should be zero-initialized
由 Niels Dekker (LKEB, Leiden University Medical Center) 報告, 2007-09-11

新版本的 value_initialized (Boost 版本1.35或以上)提供了這些問題的變通方法:value_initialized 將在構造所含對像之前清除它的內部數據。

類型和對像

模板類 value_initialized<T>

namespace boost {

template<class T>
class value_initialized
{
public :
value_initialized() : x() {}
operator T&() const { return x ; }
T& data() const { return x ; }

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() ;
}

} // 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)的值初始化提供一致的語法,為不同的值初始化提供正確的語法(請見 value-initialization 的語法)。

被包裝的對象可以通過轉換操作符 T&, 成員函數 data(), 或非成員函數 get() 進行訪問:

void watch(int);
value_initialized<int> x;

watch(x) ; // 使用 operator T&
watch(x.data());
watch( get(x) ) // 使用函數 get()

常量和非常量的對象都可以被包裝。非常量的對象可以通過包裝器直接修改,常量對像則不可以:

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: 不能轉為 int&
static_cast<int const&>(y) = 1 ; // ERROR: 不能修改常量值
get(y) = 1 ; // ERROR: 不能修改常量值

警告:

轉換操作符和 data() 成員函數都是 const 的,以允許從一個常量的包裝器來訪問被包裝的對象:

void foo(int);
value_initialized<int> const x ;
foo(x);

但是請注意,轉換操作符是轉為 T& 的,即使它本身是 const 的。因此,如果 T 是一個非常量類型,你可以通一個常量的包裝器對被包裝的對象進行修改:

value_initialized<int> const x_c ;
int& xr = x_c ; // OK, 可以轉換為 int& 即使 x_c 本身是 const 的。
xr = 2 ;

這種不正常行為的原因是,有些常用的編譯器不接受以下代碼:

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

這些編譯器認為兩個轉換操作符之間有歧義。這是不正確的,但是我所知道的解決辦法就是只提供一個轉換操作符,這就導致了前面所說的異常行為。

提議的標準:非成員函數 get() 慣用法

可以通過一個常量的包裝器修改非常量的被包裝對像這一異常行為是可以避免的,只要確保總是通過 get() 來訪問被包裝對像:

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

value_initialized<int const> cx ;
get(x) = 1 ; // ERROR: 不能修改常量對像

value_initialized<int> const x_c ;
get(x_c) = 1 ; // ERROR: 不能修改常量對像

value_initialized<int const> const cx_c ;
get(cx_c) = 1 ; // ERROR: 不能修改常量對像

initialized_value

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

initialized_value_t const initialized_value = {} ;

} // namespace boost
initialized_value 提供了一個方便的方法來獲得一個初始值:它的轉換操作符為任意可複製構造類型提供一個適當的 value-initialized 對象。假設你需要有一個類型 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, and J. Stephen Adamczyk wrote various papers, proposing to extend the support for brace-enclosed initializer lists in the next version of C++. This would allow a variable var of any DefaultConstructible type T to be value-initialized by doing T var = {}. The papers are listed at Bjarne's web page, My C++ Standards committee papers
[2] Scott Meyers, Effective C++, Third Edition, item 6, Explicitly disallow the use of compiler-generated functions you do not want, Scott Meyers: Books and CDs
[3] The C++ Standard, Second edition (2003), ISO/IEC 14882:2003
[4] POD stands for "Plain Old Data" 

鳴謝

value_initialized was developed by Fernando Cacciola, with help and suggestions from David Abrahams and Darin Adler.
Special thanks to Bjorn Karlsson who carefully edited and completed this documentation.

value_initialized was reimplemented by Fernando Cacciola and Niels Dekker for Boost release version 1.35 (2008), offering a workaround to various compiler issues.

initialized_value was written by Niels Dekker, and added to Boost release version 1.36 (2008).

Developed by Fernando Cacciola, the latest version of this file can be found at www.boost.org.


Revised 23 May 2008

c Copyright Fernando Cacciola, 2002, 2008.

Distributed under the Boost Software License, Version 1.0. See www.boost.org/LICENSE_1_0.txt