委托構造函數
在引入C++ 11之前,如果某個類有多個重載的構造函數,且這些構造函數中有一些共同的初始化邏輯,通常都需要再編寫一個帶參數的初始化函數,然後在這些構造函數中調用這個初始化函數。在C++ 11中,再也不用這麼麻煩了。我們可以實現一個最基礎的構造函數,其他構造函數都調用這個構造函數。示例代碼如下:
1 class CPerson
2 {
3 public:
4 CPerson() : CPerson(0, ) { NULL; }
5 CPerson(int nAge) : CPerson(nAge, ) { NULL; }
6 CPerson(int nAge, const string &strName)
7 {
8 stringstream ss;
9 ss << strName << is << nAge << years old.;
10 m_strInfo = ss.str();
11 }
12
13 private:
14 string m_strInfo;
15 };
統一的初始化語法
在引入C++ 11之前,有各種不同的初始化語法。在C++ 11中,仍可以使用這些初始化語法,但也可以選擇使用新引入的統一的初始化語法。統一的初始化語法用一對大括號{}表示,使用{}初始化語法還可有效地避免窄轉換。示例代碼如下:
1 int a{5};
2 char c{'X'};
3 int p[5] = {1, 2,3, 4, 5};
4 vector
5 CPerson person{10, Mike};
6 int b = 5.3; // b賦值成5,發生了窄轉換
7 int d{5.3}; // 會提示編譯錯誤,避免了窄轉換
語法甜點4:nullptr
nullptr是C++ 11中新加的一個關鍵字,用於標識空指針。引入nullptr後,可以解決某些函數重載時的二義性問題。示例代碼如下:
1 void F(int a)
2 {
3 cout << a << endl;
4 }
5
6 void F(char *p)
7 {
8 assert(p != NULL);
9
10 cout << p << endl;
11 }
12
13 int main(int argc, _TCHAR* argv[])
14 {
15 int *p = nullptr;
16 int *q = NULL;
17 bool bEqual = (p == q); // 兩個指針值是相等的,bEqual為true
18 int a = nullptr; // 編譯失敗,nullptr不是轉換為int
19
20 F(0); // 在C++ 98中編譯失敗,有二義性;在C++ 11中調用F(int)
21 F(nullptr); // 調用F(char *)
22
23 getchar();
24 return 0;
25 }
成員變量初始化
與Java和C#中的用法一樣,可以對成員變量進行就地初始化。示例代碼如下:
1 class CPerson
2 {
3 private:
4 int m_nAge = 10;
5 string m_strName = Mike;
6 };
默認或禁用函數
當我們定義了自己的帶參數的構造函數時,編譯器將不再生成默認的構造函數,如果此時想使用默認的構造函數,則必須顯式地聲明並定義不帶參數的構造函數。在C++ 11中,我們可以使用default關鍵字來表明我們希望使用默認的構造函數。類似的,當我們不想外部使用編譯器自動生成的構造函數或賦值函數時,我們一般需要將其聲明成protected或private的。在C++ 11中,我們可以使用delete關鍵字來表明我們不希望編譯器生成默認的構造函數或賦值函數。示例代碼如下:
1 class CPerson
2 {
3 public:
4 CPerson() = default;
5 CPerson(const CPerson &person) = delete;
6 };
static_assert
靜態斷言static_assert由一個常量表達式和一個字符串構成。在編譯期間,將計算常量表達式的值,如果為false,字符串將作為錯誤信息輸出。示例代碼如下:
1 char a = 10;
2 static_assert(sizeof(a)==4, a is not an integer.);
模板右邊雙括號
在C++ 98中,vector
-
繼承的構造函數
當一個派生類的某個函數隱藏了基類中的某個同名函數時,如果我們想在派生類中導出基類中的這個同名函數,可以通過using Base::Func的方式將基類中的這個同名函數引入到派生類的作用域內。當該方法只對普通成員函數有效,不能用於構造函數。在C++ 11中,如果派生類認為基類的構造函數已經足夠,則也可以使用using Base::Base的方式將基類的構造函數引入到派生類的作用域內。但需要注意的是,此時派生類中的成員變量並沒有進行初始化,所以應當對這些成員變量進行就地初始化。示例代碼如下:
1 class CBase
2 {
3 };
4
5 class CDerived : public CBase
6 {
7 public:
8 using CBase::CBase;
9 CDerived(int nData) : m_nData(nData) { NULL; }
10
11 private:
12 int m_nData = 10;
13 };
初始化列表
在引入C++ 11之前,只有數組能使用初始化列表。在C++ 11中,vector、list等各種容器以及string都可以使用初始化列表了。初始化列表對應的類為initializer_list,vector、list等各種容器以及string之所以可以使用初始化列表,是因為它們重載了參數類型為initializer_list的構造函數(稱為初始化列表構造函數)和賦值函數(稱為初始化列表賦值函數)。下面是一些使用初始化列表的例子。
1 void Print(const initializer_list
2 {
3 for (auto a : ilData)
4 {
5 cout << a << endl;
6 }
7 }
8
9 int main(int argc, _TCHAR* argv[])
10 {
11 vector
12 map
13 string strText{hello world};
14 Print({});
15 Print({1, 2});
16 Print({1, 2, 3, 4, 5});
17
18 getchar();
19 return 0;
20 }
非成員的begin和end
在C++ 03中,標准容器都提供了begin和end成員函數,但對於普通數組,則只能使用不同的寫法。比如:
1 vector
2 int a[100];
3 sort(v.begin(), v.end());
4 sort(a, a+sizeof(a)/sizeof(a[0]));
為了統一語法,C++ 11提供了非成員的begin和end函數。用法如下:
1 sort(begin(v), end(v));
2 sort(begin(a), end(a));
顯式虛函數重載
在引入C++ 11之前,基類和派生類中的虛函數很容易產生錯誤使用的情況。比如:
a、基類添加了一個虛函數,但該虛函數與派生類中的某個已有普通函數相同。
b、派生類添加了一個普通函數,但該函數與基類中的某個已有虛函數相同。
為了避免這些情況,在C++ 11中可以使用override來顯式地表明需要進行虛函數重載。比如:
1 class Base
2 {
3 virtual void some_func(float);
4 };
5
6 class Derived : public Base
7 {
8 virtual void some_func(int) override; // 將產生編譯錯誤
9 virtual void some_func(float) override; // 正確
10 };
C++ 11中還引入了final指示符,用於防止類或接口被繼承。比如:
1 class Base1 final { };
2 class Derived1 : public Base1 { }; // 將產生編譯錯誤
3 class Base2
4 {
5 virtual void f() final;
6 };
7 class Derived2 : public Base2
8 {
9 void f(); // 將產生編譯錯誤
10 };
C++ 03中,可以使用typedef給模板類指定一個新的類型名稱,但卻不能給類模板指定別名。比如:
1 template< typename first, typename second, int third>
2 class SomeType; template< typename second>
3 typedef SomeType
無限制的union
在C++ 03中,並非任意的數據類型都能做為union的成員。比方說,帶有non-trivial構造函數的類型就不能是 union 的成員。在C++ 11中,移除了所有對union的使用限制,除了其成員仍然不能是引用類型這種情況。
1 struct point
2 {
3 point() {}
4 point(int x, int y): m_x(x), m_y(y) {}
5 int m_x, m_y;
6 };
7 union
8 {
9 int z;
10 double w;
11 point p; // 在C++ 03中不合法;在C++ 11中合法
12 };
在C++ 11中,允許sizeof運算符作用在類型的數據成員上,而無須明確的對象。在C++ 03中,這是不允許的,會導致編譯錯誤。比如:
1 struct SomeType { OtherType member; };
2 sizeof(SomeType::member); // 在C++ 03中不合法;在C++ 11中合法
新的算法
C++ 11中新增了一些比較實用的算法。比如all_of、any_of、none_of、copy_n、copy_if和iota等。參考代碼如下:
1 int a[5] = {-2, -1, 0, 1, 2};
2 auto funIsPositive = [](int v){return v>0;};
3 bool bRet = all_of(a, a+5, funIsPositive); // false
4 bRet = any_of(a, a+5, funIsPositive); // true
5 bRet = none_of(a, a+5, funIsPositive); // false
6 int b[5] = {0};
7 copy_n(a, 5, b); // 將a開始的5個元素拷貝到b中
8 copy_if(a, a+5, b, funIsPositive); // 將1, 2兩個數拷貝到b中
9 iota(a, a+5, 10); // a中的每個元素加10
泛化的常數表達式
C++ 03中本來就已經具有常數表示式的概念,比如:3+5,6*7等。常數表示式對編譯器來說是優化的機會,編譯器常在編譯期運行它們並且將值存入程序中。同樣地,在許多場合下,C++規范要求使用常數表示式。比如數組大小、枚舉值等。
然而,常數表示式總是在遇到了函數調用時就終結。比如:
1 int GetFive() { return 5; }
2 int some_value[GetFive() + 5]; // 不合法
C++ 11引進關鍵字constexpr允許用戶保證函數是編譯期常數。比如:
1 constexpr int GetFive() { return 5; }
2 int some_value[GetFive() + 5];
C++ 11中引入的一個非常重要也是比較難於理解的新特性就是完美轉發(Perfect Forwarding)。完美轉發中有兩個關鍵詞:“轉發”和“完美”。
我們先來看第一個關鍵詞“轉發”,那麼在C++中,“轉發”表示什麼含義呢?轉發通常用在模板編程中,假設有一個函數F(a1, a2, ..., an),如果存在另一個函數G(a1, a2, ..., an),調用G相當於調用了F,則我們說函數G將a1, a2, ..., an等參數正確地轉發給了函數F。再來看第二個關鍵詞“完美”,“完美”轉發是相對於其他轉發方案而言的。在目前已提出的7種轉發方案中,只有完美轉發能夠正確地實現轉發的語義,其他方案都存在這樣或那樣的問題。下面一一進行介紹。
轉發方案一:使用非常量左值引用。考慮下面的代碼。
1 void F(int a)
2 {
3 cout << a << endl;
4 }
5
6 template
7 void G(A &a)
8 {
9 F(a);
10 }
使用非常量左值引用時,我們可以調用F(10),但無法調用G(10),即我們無法接收非常量右值的參數。
轉發方案二:使用常量左值引用。考慮下面的代碼。
1 void F(int &a)
2 {
3 cout << a << endl;
4 }
5
6 template
7 void G(const A &a)
8 {
9 F(a);
10 }
使用常量左值引用時,函數G可以接收任意類型的值作為參數,包括非常量左值、常量左值、非常量右值和常量右值。但當F的參數類型為非常量左值引用時,我們無法將一個常量左值引用轉發給一個非常量左值引用。
轉發方案三:使用非常量左值引用 + 常量左值引用。考慮下面的代碼。
1 template
2 void G(A &a)
3 {
4 F(a);
5 }
6
7 template
8 void G(const A &a)
9 {
10 F(a);
11 }
綜合前面兩種方案的分析結果,可以得出這種方案相當於對函數G進行了重載,此時可以接收任意類型的值作為參數,也可以順利地實現轉發。但由於使用了常量和非常量兩種形式的重載,當參數的個數N較大時,需要重載的函數會呈指數級增長(2的N次方),因此這種方案實際上是不可取的。
轉發方案四:使用常量左值引用 + const_cast。
1 template
2 void G(const A &a)
3 {
4 F(const_cast(a));
5 }
這種方案克服了方案二的缺點,現在可以將常量左值引用轉發給非常量左值引用了。但這又帶來了新的問題,假如F的參數是一個非常量左值引用,則調用G後,我們可以通過F來修改傳入的常量左值和常量右值了,而這是非常危險的。
轉發方案五:非常量左值引用 + 修改的參數推導規則。
這種方案與方案一類似,但需要修改現有的參數推導規則,即傳遞一個非常量右值給模板類型時,將它推導成常量右值,這樣就解決了方案一中無法接收非常量右值的參數的問題。但由於修改了現有的參數推導規則,因此會導致已有代碼的語義發生改變。考慮下面的代碼。
1 template
2 void F(A &a)
3 {
4 cout << void F(A& a) << endl;
5 }
6
7 void F(const long &a)
8 {
9 cout << void F(const long &a) << endl;
10 }
在未修改參數推導規則前,調用F(10)會選擇第二個重載函數,但修改後,卻會調用第一個重載函數,這就給C++帶來了兼容性的問題。
轉發方案六:右值引用。考慮下面的代碼。
1 template
2 void G(A &&a)
3 {
4 F(a);
5 }
在這種方案中,G將無法接收左值,因為不能將一個左值傳遞給一個右值引用。另外,當傳遞非常量右值時也會存在問題,因為此時a本身是一個左值,這樣當F的參數是一個非常量左值引用時,我們就可以來修改傳入的非常量右值了。
轉發方案七:右值引用 + 修改的參數推導規則。
要理解修改後的參數推導規則,我們先來看一下引用疊加規則:
1、T& + & = T&
2、T& + && = T&
3、T&& + & = T&
4、T或T&& + && = T&&
修改後的針對右值引用的參數推導規則為:若函數模板的模板參數為A,模板函數的形參為A&&,則可分為兩種情況討論:
1、若實參為T&,則模板參數A應被推導為引用類型T&。(由引用疊加規則第2點T& + && = T&和A&&=T&,可得出A=T&)
2、若實參為T&&,則模板參數A應被推導為非引用類型T。(由引用疊加規則第4點T或T&& + && = T&&和A&&=T&&,可得出A=T或T&&,強制規定A=T)
應用了新的參數推導規則後,考慮下面的代碼。
1 template
2 void G(A &&a)
3 {
4 F(static_cast(a));
5 }
當傳給G一個左值(類型為T)時,由於模板是一個引用類型,因此它被隱式裝換為左值引用類型T&,根據推導規則1,模板參數A被推導為T&。這樣,在G內部調用F(static_cast(a))時,static_cast(a)等同於static_cast
當傳給G一個右值(類型為T)時,由於模板是一個引用類型,因此它被隱式裝換為右值引用類型T&&,根據推導規則2,模板參數A被推導為T。這樣,在G內部調用F(static_cast(a))時,static_cast(a)等同於static_cast
可見,使用該方案後,左值和右值都能正確地進行轉發,並且不會帶來其他問題。另外,C++ 11為了方便轉發的實現,提供了一個函數模板forward,用於參數的完美轉發。使用forward後的代碼可簡化為:
1 template
2 void G(A &&a)
3 {
4 F(forward(a));
5 }
為了便於進行各種轉發方案的比較,下面以表格的形式列出了各自的特性。
轉發方案非常量左值常量左值非常量右值常量右值修改語言已知問題
一、非常量左值引用非常量左值常量左值無法轉發常量左值否無法接收非常量右值的參數
二、常量左值引用常量左值常量左值常量左值常量左值否無法將常量左值引用轉發給非常量左值引用
三、非常量左值引用 + 常量左值引用非常量左值常量左值常量左值常量左值否重載函數過多,實際編碼不可行
四、常量左值引用 + const_cast非常量左值非常量左值非常量左值非常量左值否可修改常量左值和常量右值,不安全
五、非常量左值引用 + 修改的參數推導規則非常量左值常量左值常量左值常量左值是會導致兼容性問題,且不支持移動語義
六、右值引用無法轉發無法轉發非常量左值常量左值是可修改非常量右值,不安全
七、右值引用 + 修改的參數推導規則非常量左值常量左值非常量右值常量右值是暫無,故簡稱為完美轉發