重點
auto
類型推導- 范圍
for
迭代 - 初始化列表
- 變參模板
新類型
C++11新增了類型 long long 和 unsigned long long,以支持64位(或更寬)的整型;新增了類型 char16_t和 char32_t,以支持 16位和 32 位的字符表示;還新增了“原始”字符串。
常量
nullptr
nullptr
出現的目的是為了替代 NULL
。 C 與 C++ 語言中有空指針常量,它們能被隱式轉換成任何指針類型的空指針值,或 C++ 中的任何成員指針類型的空成員指針值。 NULL
由標準庫實現提供,并被定義為實現定義的空指針常量。在 C 中,有些標準庫會把 NULL
定義為 ((void*)0)
而有些將它定義為 0
。
C++ 不允許直接將 void *
隱式轉換到其他類型,從而 ((void*)0)
不是 NULL
的合法實現。如果標準庫嘗試把 NULL
定義為 ((void*)0)
,那么下面這句代碼中會出現編譯錯誤:
char *ch = NULL;
沒有了 void *
隱式轉換的 C++ 只好將 NULL
定義為 0
。而這依然會產生新的問題,將 NULL
定義成 0
將導致 C++
中重載特性發生混亂。考慮下面這兩個 foo
函數:
void foo(char*);
void foo(int);
那么 foo(NULL);
這個語句將會去調用 foo(int)
,從而導致代碼違反直覺。
為了解決這個問題,C++11 引入了 nullptr
關鍵字,專門用來區分空指針、0
。而 nullptr
的類型為 nullptr_t
,能夠隱式的轉換為任何指針或成員指針的類型,也能和他們進行相等或者不等的比較。
#include <iostream>
#include <type_traits>void foo(char *);
void foo(int);int main() {if (std::is_same<decltype(NULL), decltype(0)>::value)std::cout << "NULL == 0" << std::endl;if (std::is_same<decltype(NULL), decltype((void*)0)>::value)std::cout << "NULL == (void *)0" << std::endl;if (std::is_same<decltype(NULL), std::nullptr_t>::value)std::cout << "NULL == nullptr" << std::endl;foo(0); // 調用 foo(int)// foo(NULL); // 該行不能通過編譯foo(nullptr); // 調用 foo(char*)return 0;
}void foo(char *) {std::cout << "foo(char*) is called" << std::endl;
}
void foo(int i) {std::cout << "foo(int) is called" << std::endl;
}
將輸出:?
foo(int) is called
foo(char*) is called
從輸出中我們可以看出,NULL
不同于 0
與 nullptr
。所以,請養成直接使用 nullptr
的習慣。
此外,在上面的代碼中,我們使用了 decltype
和 std::is_same
這兩個屬于現代 C++ 的語法,簡單來說,decltype
用于類型推導,而 std::is_same
用于比較兩個類型是否相同,我們會在后面 decltype 一節中詳細討論。
?constexp
C++ 本身已經具備了常量表達式的概念,比如 1+2
, 3*4
這種表達式總是會產生相同的結果并且沒有任何副作用。如果編譯器能夠在編譯時就把這些表達式直接優化并植入到程序運行時,將能增加程序的性能。一個非常明顯的例子就是在數組的定義階段:
#include <iostream>
#define LEN 10int len_foo() {int i = 2;return i;
}
constexpr int len_foo_constexpr() {return 5;
}constexpr int fibonacci(const int n) {return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}int main() {char arr_1[10]; // 合法char arr_2[LEN]; // 合法int len = 10;// char arr_3[len]; // 非法const int len_2 = len + 1;constexpr int len_2_constexpr = 1 + 2 + 3;// char arr_4[len_2]; // 非法char arr_4[len_2_constexpr]; // 合法// char arr_5[len_foo()+5]; // 非法char arr_6[len_foo_constexpr() + 1]; // 合法std::cout << fibonacci(10) << std::endl;// 1, 1, 2, 3, 5, 8, 13, 21, 34, 55std::cout << fibonacci(10) << std::endl;return 0;
}
上面的例子中,char arr_4[len_2]
可能比較令人困惑,因為 len_2
已經被定義為了常量。為什么 char arr_4[len_2]
仍然是非法的呢?這是因為 C++ 標準中數組的長度必須是一個常量表達式,而對于 len_2
而言,這是一個 const
常數,而不是一個常量表達式,因此(即便這種行為在大部分編譯器中都支持,但是)它是一個非法的行為,我們需要使用接下來即將介紹的 C++11 引入的 constexpr
特性來解決這個問題;而對于 arr_5
來說,C++98 之前的編譯器無法得知 len_foo()
在運行期實際上是返回一個常數,這也就導致了非法的產生。
注意,現在大部分編譯器其實都帶有自身編譯優化,很多非法行為在編譯器優化的加持下會變得合法,若需重現編譯報錯的現象需要使用老版本的編譯器。
C++11 提供了 constexpr
讓用戶顯式的聲明函數或對象構造函數在編譯期會成為常量表達式,這個關鍵字明確的告訴編譯器應該去驗證 len_foo
在編譯期就應該是一個常量表達式。?
此外,constexpr
修飾的函數可以使用遞歸:
constexpr int fibonacci(const int n) {return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}
?從 C++14 開始,constexpr
函數可以在內部使用局部變量、循環和分支等簡單語句,例如下面的代碼在 C++11 的標準下是不能夠通過編譯的:
constexpr int fibonacci(const int n) {if(n == 1) return 1;if(n == 2) return 1;return fibonacci(n-1) + fibonacci(n-2);
}
為此,我們可以寫出下面這類簡化的版本來使得函數從 C++11 開始即可用:
constexpr int fibonacci(const int n) {return n == 1 || n == 2 ? 1 : fibonacci(n-1) + fibonacci(n-2);
}
變量及其初始化?
if/switch 變量聲明強化
在傳統 C++ 中,變量的聲明雖然能夠位于任何位置,甚至于 for
語句內能夠聲明一個臨時變量 int
,但始終沒有辦法在 if
和 switch
語句中聲明一個臨時的變量。例如:
#include <iostream>
#include <vector>
#include <algorithm>int main() {std::vector<int> vec = {1, 2, 3, 4};// 在 c++17 之前const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 2);if (itr != vec.end()) {*itr = 3;}// 需要重新定義一個新的變量const std::vector<int>::iterator itr2 = std::find(vec.begin(), vec.end(), 3);if (itr2 != vec.end()) {*itr2 = 4;}// 將輸出 1, 4, 3, 4for (std::vector<int>::iterator element = vec.begin(); element != vec.end(); ++element)std::cout << *element << std::endl;
}
在上面的代碼中,我們可以看到 itr
這一變量是定義在整個 main()
的作用域內的,這導致當我們需要再次遍歷整個 std::vector
時,需要重新命名另一個變量。C++17 消除了這一限制,使得我們可以在 if
(或 switch
)中完成這一操作:
// 將臨時變量放到 if 語句內
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);itr != vec.end()) {*itr = 4;
}
初始化列表
初始化是一個非常重要的語言特性,最常見的就是在對象進行初始化時進行使用。
在傳統 C++ 中,不同的對象有著不同的初始化方法,例如普通數組、
POD (Plain Old Data,即沒有構造、析構和虛函數的類或結構體)
類型都可以使用 {}
進行初始化,也就是我們所說的初始化列表。
而對于類對象的初始化,要么需要通過拷貝構造、要么就需要使用 ()
進行。
這些不同方法都針對各自對象,不能通用。例如:
#include <iostream>
#include <vector>class Foo {
public:int value_a;int value_b;Foo(int a, int b) : value_a(a), value_b(b) {}
};int main() {// before C++11int arr[3] = {1, 2, 3};Foo foo(1, 2);std::vector<int> vec = {1, 2, 3, 4, 5};std::cout << "arr[0]: " << arr[0] << std::endl;std::cout << "foo:" << foo.value_a << ", " << foo.value_b << std::endl;for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {std::cout << *it << std::endl;}return 0;
}
為解決這個問題,C++11 首先把初始化列表的概念綁定到類型上,稱其為 std::initializer_list
,允許構造函數或其他函數像參數一樣使用初始化列表,這就為類對象的初始化與普通數組和 POD 的初始化方法提供了統一的橋梁,例如:
#include <initializer_list>
#include <vector>
#include <iostream>class MagicFoo {
public:std::vector<int> vec;MagicFoo(std::initializer_list<int> list) {for (std::initializer_list<int>::iterator it = list.begin();it != list.end(); ++it)vec.push_back(*it);}
};
int main() {// after C++11MagicFoo magicFoo = {1, 2, 3, 4, 5};std::cout << "magicFoo: ";for (std::vector<int>::iterator it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) std::cout << *it << std::endl;
}
這種構造函數被叫做初始化列表構造函數,具有這種構造函數的類型將在初始化時被特殊關照。
初始化列表除了用在對象構造上,還能將其作為普通函數的形參,例如:
public:void foo(std::initializer_list<int> list) {for (std::initializer_list<int>::iterator it = list.begin();it != list.end(); ++it) vec.push_back(*it);}magicFoo.foo({6,7,8,9});
其次,C++11 還提供了統一的語法來初始化任意的對象,例如:
Foo foo2 {3, 4};
?結構化綁定
結構化綁定提供了類似其他語言中提供的多返回值的功能。在容器一章中,我們會學到 C++11 新增了 std::tuple
容器用于構造一個元組,進而囊括多個返回值。但缺陷是,C++11/14 并沒有提供一種簡單的方法直接從元組中拿到并定義元組中的元素,盡管我們可以使用 std::tie
對元組進行拆包,但我們依然必須非常清楚這個元組包含多少個對象,各個對象是什么類型,非常麻煩。
C++17 完善了這一設定,給出的結構化綁定可以讓我們寫出這樣的代碼:
#include <iostream>
#include <tuple>std::tuple<int, double, std::string> f() {return std::make_tuple(1, 2.3, "456");
}int main() {auto [x, y, z] = f();std::cout << x << ", " << y << ", " << z << std::endl;return 0;
}
類型推導?
在傳統 C 和 C++ 中,參數的類型都必須明確定義,這其實對我們快速進行編碼沒有任何幫助,尤其是當我們面對一大堆復雜的模板類型時,必須明確的指出變量的類型才能進行后續的編碼,這不僅拖慢我們的開發效率,也讓代碼變得又臭又長。
C++11 引入了 auto
和 decltype
這兩個關鍵字實現了類型推導,讓編譯器來操心變量的類型。這使得 C++ 也具有了和其他現代編程語言一樣,某種意義上提供了無需操心變量類型的使用習慣。
auto
auto
在很早以前就已經進入了 C++,但是他始終作為一個存儲類型的指示符存在,與 register
并存。在傳統 C++ 中,如果一個變量沒有聲明為 register
變量,將自動被視為一個 auto
變量。而隨著 register
被棄用(在 C++17 中作為保留關鍵字,以后使用,目前不具備實際意義),對 auto
的語義變更也就非常自然了。
使用 auto
進行類型推導的一個最為常見而且顯著的例子就是迭代器。你應該在前面的小節里看到了傳統 C++ 中冗長的迭代寫法:
// 在 C++11 之前
// 由于 cbegin() 將返回 vector<int>::const_iterator
// 所以 it 也應該是 vector<int>::const_iterator 類型
for(vector<int>::const_iterator it = vec.cbegin(); it != vec.cend(); ++it)
而有了 auto
之后可以:
#include <initializer_list>
#include <vector>
#include <iostream>class MagicFoo {
public:std::vector<int> vec;MagicFoo(std::initializer_list<int> list) {// 從 C++11 起, 使用 auto 關鍵字進行類型推導for (auto it = list.begin(); it != list.end(); ++it) {vec.push_back(*it);}}
};
int main() {MagicFoo magicFoo = {1, 2, 3, 4, 5};std::cout << "magicFoo: ";for (auto it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) {std::cout << *it << ", ";}std::cout << std::endl;return 0;
}
一些其他的常見用法:
auto i = 5; // i 被推導為 int
auto arr = new auto(10); // arr 被推導為 int *
?從 C++ 14 起,auto
能用于 lambda 表達式中的函數傳參,而 C++ 20 起該功能推廣到了一般的函數。考慮下面的例子:
auto add14 = [](auto x, auto y) -> int {return x+y;
}int add20(auto x, auto y) {return x+y;
}auto i = 5; // type int
auto j = 6; // type int
std::cout << add14(i, j) << std::endl;
std::cout << add20(i, j) << std::endl;
注意:auto
還不能用于推導數組類型:
auto auto_arr2[10] = {arr}; // 錯誤, 無法推導數組元素類型2.6.auto.cpp:30:19: error: 'auto_arr2' declared as array of 'auto'
auto auto_arr2[10] = {arr};
decltype
decltype
關鍵字是為了解決 auto
關鍵字只能對變量進行類型推導的缺陷而出現的。它的用法和 typeof
很相似:
decltype(表達式)
有時候,我們可能需要計算某個表達式的類型,例如:
auto x = 1;
auto y = 2;
decltype(x+y) z;
你已經在前面的例子中看到 decltype
用于推斷類型的用法,下面這個例子就是判斷上面的變量 x, y, z
是否是同一類型:
if (std::is_same<decltype(x), int>::value)std::cout << "type x == int" << std::endl;
if (std::is_same<decltype(x), float>::value)std::cout << "type x == float" << std::endl;
if (std::is_same<decltype(x), decltype(z)>::value)std::cout << "type z == type x" << std::endl;
其中,std::is_same<T, U>
用于判斷 T
和 U
這兩個類型是否相等。輸出結果為:
type x == int
type z == type x
你可能會思考, auto
能不能用于推導函數的返回類型呢?還是考慮一個加法函數的例子,在傳統 C++ 中我們必須這么寫:
template<typename R, typename T, typename U>
R add(T x, U y) {return x+y;
}
注意:typename 和 class 在模板參數列表中沒有區別,在 typename 這個關鍵字出現之前,都是使用 class 來定義模板參數的。但在模板中定義有嵌套依賴類型的變量時,需要用 typename 消除歧義
這樣的代碼其實變得很丑陋,因為程序員在使用這個模板函數的時候,必須明確指出返回類型。但事實上我們并不知道 add()
這個函數會做什么樣的操作,以及獲得一個什么樣的返回類型。
在 C++11 中這個問題得到解決。雖然你可能馬上會反應出來使用 decltype
推導 x+y
的類型,寫出這樣的代碼:
decltype(x+y) add(T x, U y)
但事實上這樣的寫法并不能通過編譯。這是因為在編譯器讀到 decltype(x+y) 時,x
和 y
尚未被定義。為了解決這個問題,C++11 還引入了一個叫做尾返回類型(trailing return type),利用 auto
關鍵字將返回類型后置:
template<typename T, typename U>
auto add2(T x, U y) -> decltype(x+y){return x + y;
}
令人欣慰的是從 C++14 開始是可以直接讓普通函數具備返回值推導,因此下面的寫法變得合法:
template<typename T, typename U>
auto add3(T x, U y){return x + y;
}
可以檢查一下類型推導是否正確:
// after c++11
auto w = add2<int, double>(1, 2.0);
if (std::is_same<decltype(w), double>::value) {std::cout << "w is double: ";
}
std::cout << w << std::endl;// after c++14
auto q = add3<double, int>(1.0, 2);
std::cout << "q: " << q << std::endl;
decltype(auto)
decltype(auto)
是 C++14 開始提供的一個略微復雜的用法。
要理解它你需要知道 C++ 中參數轉發的概念,我們會在語言運行時強化一章中詳細介紹,你可以到時再回來看這一小節的內容。
簡單來說,decltype(auto)
主要用于對轉發函數或封裝的返回類型進行推導,它使我們無需顯式的指定 decltype
的參數表達式。考慮看下面的例子,當我們需要對下面兩個函數進行封裝時:
std::string lookup1();
std::string& lookup2();
在 C++11 中,封裝實現是如下形式:
std::string look_up_a_string_1() {return lookup1();
}
std::string& look_up_a_string_2() {return lookup2();
}
?而有了 decltype(auto)
,我們可以讓編譯器完成這一件煩人的參數轉發:
decltype(auto) look_up_a_string_1() {return lookup1();
}
decltype(auto) look_up_a_string_2() {return lookup2();
}
尾返回類型推導
你可能會思考, auto
能不能用于推導函數的返回類型呢?還是考慮一個加法函數的例子,在傳統 C++ 中我們必須這么寫:
template<typename R, typename T, typename U>
R add(T x, U y) {return x+y;
}
注意:typename 和 class 在模板參數列表中沒有區別,在 typename 這個關鍵字出現之前,都是使用 class 來定義模板參數的。但在模板中定義有嵌套依賴類型的變量時,需要用 typename 消除歧義
這樣的代碼其實變得很丑陋,因為程序員在使用這個模板函數的時候,必須明確指出返回類型。但事實上我們并不知道 add()
這個函數會做什么樣的操作,以及獲得一個什么樣的返回類型。
在 C++11 中這個問題得到解決。雖然你可能馬上會反應出來使用 decltype
推導 x+y
的類型,寫出這樣的代碼: