()和{}初始化的用法

Damon_liufb發表於2020-10-09
  • 大括號初始化可以應用的語境最為寬泛。可以避免令人苦惱的解析語法、可以阻止隱式窄化型別轉換
  • 建構函式過載決議期間,只要有任何可能,大括號初始化物就會與帶有std::initializer_list型別的形參想匹配,即使其他過載版本更合適
  • 使用兩個實參來建立std::vector<數值型別>結果會大相徑庭。這是大括號與小括號之間的一個明顯不同的例子

我們指定初始化的方式包括使用小括號、使用等號或者使用大括號。

int x(0);	//使用小括號初始化
int y = 0;	//使用等號初始化
int z{0};	//使用大括號初始化

int z = {0};	//等號加大括號的用法等同於使用大括號

很多人喜歡使用等號來書寫初始化語句,新手往往會認為這裡面會發生一次賦值,但是實際上是沒有的。我們使用int等內建型別不需要區分的這麼開,但是當我們使用自定義型別的時候,就必須區分開初始化和賦值的概念了。

Widget w1;		//呼叫的是預設建構函式

Widget w2 = w1;	//是初始化而並非賦值,呼叫的是複製建構函式

w1 = w2;		//並非賦值,呼叫的是複製賦值運算子

C++11中引入了統一初始化:單一的、至少從概念上可以用於一切場合、表達一切意思的初始化。它的基礎是大括號形式。

大括號初始化可以表達之前無法表達之事。

使用大括號來指定容器的初始內容非常簡單:

std::vector<int> v{1,3,5};  //v的初始內容為1,3,5

大括號初始化可以為非靜態成員指定預設初始化值,C++11中也可以使用“=”初始化語法,但是不能使用小括號:

class Widget{
private:
	int x{0};  
	int y = 0;
	//int z(0);		//錯誤
};

不可複製的物件可以採用大括號和小括號來初始化,但是不能使用“=”

std::atomic<int> ai1{0};
std::atomic<int> ai2(0);
//std::atomic<int> ai3 = 0;    //錯誤

通過上述情況我們可以看到,這三種初始化方法中,只有大括號初始化方法適用所有場合。

不過大括號初始化有一項新特性,就是它禁止內建型別之間進行隱式窄化型別轉換。如果大括號內的表示式無法保證能夠採用進行初始化的物件來表達,則程式碼不能通過編譯,不過小括號和等號可以:

double x,y,z;

//int sum{x + y + z};			//錯誤,double型別之和可能無法通過int表達
int sum2(x + y + z);
int sum3 = x+y+z;

大括號初始化的一項特徵是,它對於C++的最令人苦惱的解析語法免疫。C++規定:任何能夠解析為宣告的都要解析為宣告,而這會帶來副作用。程式設計師本來想要以預設方式構造一個物件,結果卻不小心宣告瞭一個函式。這個錯誤的根本原因在於建構函式呼叫語法。

//以傳遞引數方式呼叫建構函式
Widget w1(10);		//呼叫Widget的建構函式,傳入形參10

//如果呼叫一個沒有形參的Widget建構函式的話,結果卻變成了宣告一個函式而非物件
Widget w2();		//宣告瞭一個名為w2,返回一個Widget型別物件的函式

由於函式宣告不能使用大括號來指定形參列表,所以使用大括號來完成物件的預設構造沒有這個問題:

Widget w3{};		//呼叫沒有形參的建構函式

大括號初始化存在一些缺陷,伴隨大括號初始化有時會出現意外行為。這種行為源於大括號初始化物、std::initializer_list以及建構函式過載決議之間的糾結關係。這幾者之間的相互作用可以使程式碼看起來要做某一件事,實際上卻在做另一件事。比如使用大括號初始化物來初始化一個使用auto宣告的變數,那麼推匯出來的型別就會變成std::initializer_list。

在建構函式被呼叫時,只要形參中沒有任何一個具備std::initializer_list型別,那麼小括號和大括號的意義就沒有區別。如果一個或多個建構函式宣告瞭任何一個具備std::initializer_list型別的形參,那麼採用大括號初始化語法的呼叫語句會優先選用帶有std::initializer_list型別形參的過載版本。即使是平常會執行復制或移動的建構函式也會被帶有std::initializer_list型別形參的建構函式劫持:

//建構函式沒有std::initializer_list型別,大括號和小括號初始化沒有區別
class Widget{
public:
	Widget(int i,bool b);
	Widget(int i,double d);
};

Widget w1(10,true);		//呼叫第一個建構函式
Widget w2{10,true};		//呼叫第一個建構函式

Widget w3(10,5.0);		//呼叫第二個建構函式
Widget w4{10,5.0};		//呼叫第二個建構函式
//建構函式一旦有std::initializer_list形參,那麼大括號初始化一定會選用這個建構函式
class Widget{
public:
	Widget(int i,bool b);
	Widget(int i,double d);
	Widget(std::intializer_list<long double> il);

	operator float() const ;   //強制轉換成float型別
};

Widget w1(10,true);		//呼叫第一個建構函式
Widget w2{10,true};		//使用大括號,呼叫第三個建構函式,10和true被強制轉換為long double

Widget w3(10,5.0);		//呼叫第二個建構函式
Widget w4{10,5.0};		//使用大括號,呼叫第三個建構函式,10和5.0被強制轉換為long double

Widget w5(w4);		//使用小括號,呼叫的是複製建構函式
Widget w6{w4};		//使用大括號,呼叫第三個建構函式
					//w4的返回值被強制轉換float,而float又被強制轉換為long double

Widget w7(std::move(w4));		//使用小括號,呼叫的是移動建構函式
Widget w8{std::move(w4)};		//使用大括號,呼叫第三個構造,和w6結果相同

這種優先呼叫是很強烈的,即使最優選的呼叫std::initializer_list的建構函式無法被呼叫,編譯器還是會選擇這個。只有在找不到任何辦法把大括號初始化物中的實參轉化成std::initializer_list模板中的型別時,編譯器才會去檢查普通的過載函式。

class Widget{
public:
	Widget(int i,bool b);
	Widget(int i,double d);
	Widget(std::initializer_list<bool> il);
};

Widget w{10,5.0};		//無法通過編譯,無法把10和5.0窄化為bool
class Widget{
public:
	Widget(int i,bool b);
	Widget(int i,double d);
	Widget(std::initializer_list<std::string> il);
};

Widget w{10,5.0};	//呼叫第二個建構函式,因為編譯器無法把int和double轉換成string型別,所以尋找過載函式

對於std::initializer_list還有個小問題,當我們使用一對空大括號來構造一個物件,而該物件既支援預設的構造,也支援帶有std::initializer_list型別引數的構造。此時的這對空大括號的意思是“沒有實參”而不是“空的std::initializer_list”。如果我們想要傳入一個空的std::initializer_list,可以通過把空大括號作為建構函式實參的方式實現,即把一對空大括號放入一對小括號或大括號。

class Widget{
public:
	Widget();    //預設構造
	Widget(std::initializer_list<int> il);
};

Widget w1;		//呼叫預設構造
Widget w2{};	//呼叫預設構造
Widget w3();	//解析語法,變成函式宣告而不是呼叫構造
Widget w4({});	//呼叫帶有std::initializer_list引數的構造,傳入空的std::initializer_list
Widget w5{{}};	//同上

  大括號初始化物、std::initializer_list、建構函式過載決議,這些內容不注意會有很大的影響。直接影響到的就是std::vector類。std::vector類中有一個形參中沒有std::initializer_list型別的建構函式,它允許我們指定容器的初始尺寸,以及一個初始化時讓所有元素擁有的值。但是它還有一個帶有一個std::initializer_list型別形參的建構函式,允許我們逐個指定容器中的元素值。如果我們要建立一個元素為數值型別的std::vector,並傳遞了兩個實參給建構函式的話,用小括號還是大括號結果會大相徑庭:

//小括號,呼叫了形參中沒有一個具備std::initializer_list型別的建構函式
//結果是建立了一個含有10個元素的std::vector,所有的元素值都是20
std::vector<int> v1(10,20);


//大括號,呼叫了形參中含有std::initializer_list型別的建構函式
//結果是建立了一個含有2個元素的std::vector,元素的值分別為10和20
std::vector<int> v2{10,20};

所以從某種程度上說vector的設計是有缺陷的。我們自己在設計一個類的時候,我們需要意識到自己撰寫的一組過載建構函式中只要有std::initializer_list形參,則使用大括號初始化的客戶程式碼只會發現這些構造的過載版本。

相關文章