C++11新特性選講 語言部分 侯捷
本課程分為兩個部分:語言的部分和標準庫的部分。只談新特性,并且是選講。
本文為語言部分筆記。
- 語言
- Variadic Templates
- move semantics
- auto
- Range-based for loop
- Initializer list
- Lambdas
- …
- 標準庫
- type_traits
- unodered containers
- forward_list
- array
- tuple
- concurrency
- RegEx
- …
關于頭文件
C++11的新特性包含語言和標準庫兩部分,后者以頭文件 header files 的形式呈現。
關于C++的頭文件,有以下幾點:
- C++標準庫的頭文件均不帶 .h,如
#include <iostream>
- 在C++中,舊式的C的頭文件(帶有 .h)依然可用,如
#include <stdio.h>
- 建議在C++中使用,新式的C頭文件,與舊式的關系:xxx.h -> cxxx,如
#include <cstdio>
Variatic Templates
函數變參模板
假如我想設計一個函數 print,它能夠接收任意數量的參數,并且這個參數的類型也是任意的,就可以利用 Variatic Templates 來遞歸地實現:
#include <iostream>
void print() {} // 1template<typename T, typename... Types> // 2
void print(const T& firstArg, const Types&... args) { // 3std::cout << firstArg << std::endl;print(args...); // 4
}int main() {print("dfafda", 's', 123);
}
注意這里的 ...
可不是我們口語中的省略號,而是實實在在的C++11新語法的一部分,可以將它理解為一個 pack (包),具體是什么 “包”,則取決于它出現在哪里。在本例中,...
共出現了三次:
- 用于 template parameters,就是 template parameters pack,”模板參數包“,如2處
- 用于 function parameters types,就是 function parameters types pack,”函數參數類型包“,如3處
- 用于 function parameters,就是 function parameters pack,“函數參數包”,如4處
在變參模板中,如果我們想要知道可變參數的個數,可通過:sizeof...(args)
。
注意除了2處, 我們在1處定義了一個空參數列表的 print 函數的重載版本,它在 print 函數地參數列表中的參數被遞歸地打印完之后被調用,其實就是相當于我們 print 函數的遞歸退出的條件。
思考:以下這個 print 函數的重載版本能夠與上面的 print 函數并存嗎,如果可以,誰比較泛化,誰比較特化呢?(我們知道,兩個版本均可的情況下,較為特化的版本會被優先調用)
template <typename... Types>
void print(const Types&... args) {/* --- */
}
變參模板的花式應用:
- 萬能的hash function:多種函數重載 + 遞歸 + 函數變參模板 —>花式調用
- tuple:類變參模板 + 繼承
類變參模板
一些”小“的新特性
Spaces in Template Expressions
在C++11之前,如果有模板嵌套,右側的兩個尖括號不能靠在一起,中間須有空格,否則編譯器會認為那是個右移運算符,在C++11之后編譯器變聰明了,不再需要這個空格。
vector<list<int> >; // OK in each C++ version
vector<list<int>>; // OK since C++ 11
nullptr and std::nullptr_t
在C++11之后,我們可以使用 nullptr
(而非之前的 NULL
或者 0)來表示空指針。注意 NULL
就是一個宏,其值為0,而 nullptr
確實是個指針,其類型為 std::nullptr_t
。下面的例子可以驗證:
#include <iostream>void f(int) {std::cout << "call f(int)" << std::endl;
}void f(void*) {std::cout << "call f(void*)" << std::endl;
}int main() {f(0); // calls f(int)f(NULL); // calls f(int) if NULL is 0; ambiguous otherwisef(nullptr); // calls f(void*)
}
auto
自動類型推導 auto:在C++11之后,可以用 auto
來定義變量的類型,編譯器會自動進行類型推導。
auto i = 42; // i是int類型
double f();
auto d = f(); // d是double類型
注意:不建議在任何時候都使用 auto ,而是推薦在這個變量的類型名稱實在是很長或者很復雜,實在是懶得打那么多字時使用,但是我們要知道變量應該是什么類型,如:
vector<string> v;
auto pos = v.begin(); // 過長auto f = [](int x) -> bool { // 過于復雜// ...
}
程序員要做到對自己的變量類型心中有數。
Uniform Initialization
在C++11之前,許多程序員會疑惑,一個變量或者對象的初始化可能會發生于小括號,大括號,賦值運算符。如:
vector<int> vec(3, 5);
vector<int> vec {1, 2 ,3};
int a = 1;
C++11引入一致初始化,使用大括號,在變量后面直接跟大括號,大括號中可以有任意多個元素個數,設置初值,進行初始化,如:
int values[] {1, 2, 3};
vector<int> v {2, 3, 4};
complex<double> {4.0, 3.0};
當然之前的語法也是可用的。
實際上,編譯器看到 {}
就會作出一個 initializer_list<T>
,它關聯至一個 array<T, n>
。調用函數(如ctor)時該 array 的元素被編譯器分解逐一傳給函數。需要注意的是:若某個函數參數就是個 initializer_list<T>
,調用者不能傳遞數個 T 參數然后以為它們會被自動轉換為一個 intializer_list<T>
傳入,即需要自己手動將數個參數轉換為 initializer_list<T>
再進行傳值。
比如:
vector<string> cities {"Berlin", "New York", "London"};
這形成一個 initializer_list<string>
,背后有個 array<string, 3>
。調用 vector<string>
的 ctors(構造函數)中的接收 initialize_list<string>
的版本,標準庫中所有容器都有接收這個 initializer_list<T>
的構造函數。
但是對于我們自己的類,可能沒有接收 intializer_list<T>
這種參數的構造函數,此時這個初始化列表逐一分解拆成一個一個的參數傳給函數,再去找與多個單個參數相匹配的構造函數。
initializer_list
初始化列表是支持上面提到的大括號形式的一致性初始化的背后方法。
為了支持用戶自定義的類的 initializer_list。C++11提供了一個類模板:std::initializer_list<T>
。他可以用用于使用一包參數值來進行初始化,或者用來其他你想要處理一包參數的地方。如使用initalizer_list傳參:
#include <iostream>void print(std::initializer_list<int> vals) {for (auto ite = vals.begin(); ite!=vals.end(); ++ite) {std::cout << *ite << "\n";}
}int main() {print( {1,2,3,4} ); // 使用initalizer_list傳參
}
-
即
{}
即可形成一個 initializer_list -
不同于前面的 variadic template,這里的 initializer_list 需要的是固定類型 T 的任意多個參數。也可以看做是一種容器。
-
initializer_list背后由array構建。
-
intializer_list如果被拷貝,會是淺拷貝(引用語義)
在C++11之后的標準庫中,initializers_list
有許多應用,最常見的肯定是上面提到過的各個容器的構造函數中可以使用其作為參數。另外,在一些算法中也有應用,比如 min/max 函數,在C++11之前,它們只能支持兩個元素的比較:
std::min(1, 2);
在C++11之后,借助 initializer_list 它可以支持多個元素的比較:
std::min( {1, 2, 3, 4} );
range-based for loop
在C++11之后
for (decl : coll) {statement;
}
如:
std::vector<int> vec = {1, 2, 3, 4};
for (int i : vec) {std::cout << i << std::endl;
}
也可以用引用:
std::vector<double> vec;
for (auto& elem : vec) {elem *= 3; // 因為是引用,所以會改變原vector
}
類似python的for loop:
for i in range(10):print(i)
實際上,這種for loop的背后實現就是將該容器的迭代器取出來,并遍歷一遍,并將遍歷過程中的每個元素賦值到左側聲明出來的變量。
這種for loop賦值時可能會做隱式類型轉換。
=default, =delete
如果你自行定義了一個 ctor,那么編譯器就不會再給你一個 default ctor;但是如果你強制加上 =default
(可以空格),就可以重新獲得并使用默認的 default ctor。而如果加上 =delete
,則是禁用該成員函數的使用。
class Zoo {
public:Zoo(int i1, int i2) : d1(i1), d2(i2) {} // 構造函數Zoo(const Zoo&) = delete; // 拷貝構造Zoo(Zoo&&) = default; // 移動構造Zoo& operator=(const Zoo&) = default; // 拷貝賦值Zoo& operator=(const Zoo&&) = delete; // 移動賦值virtual ~Zoo() {} // 析構函數
private:int d1, d2;
}
=default
每當我們聲明一個有參構造函數時,編譯器就不會創建默認構造函數。在這種情況下,我們可以使用 =default
說明符來創建默認的構造函數。以下代碼演示了如何創建:
// use of defaulted functions
#include <iostream>
using namespace std;class A {
public:// A user-definedA(int x){cout << "This is a parameterized constructor";}// Using the default specifier to instruct// the compiler to create the default implementation of the constructor.A() = default;
};int main(){A a; //call A()A x(1); //call A(int x)cout<<endl;return 0;
}
=delete
在C ++ 11之前,操作符delete 只有一個目的,即釋放已動態分配的內存。而C ++ 11標準引入了此操作符的另一種用法,即:禁用成員函數的使用。這是通過附加 = delete
來完成的; 說明符到該函數聲明的結尾。
使用 = delete
說明符禁用其使用的任何成員函數稱為explicitly deleted函數。
雖然不限于它們,但這通常是針對隱式函數。以下示例展示了此功能派上用場的一些任務:
禁用拷貝構造函數
// copy-constructor using delete operator
#include <iostream>
using namespace std; class A {
public: A(int x): m(x) { } // Delete the copy constructor A(const A&) = delete; // Delete the copy assignment operator A& operator=(const A&) = delete; int m;
}; int main() { A a1(1), a2(2), a3(3); // Error, the usage of the copy assignment operator is disabled a1 = a2; // Error, the usage of the copy constructor is disabled a3 = A(a2); return 0;
}
禁用不需要的類型轉換
// type conversion using delete operator
#include <iostream>
using namespace std;
class A {
public: A(int) {} // Declare the conversion constructor as a deleted function. Without this step, // even though A(double) isn't defined, the A(int) would accept any double value// for it's argumentand convert it to an int A(double) = delete;
};
int main() { A A1(1); // Error, conversion from double to class A is disabled. A A2(100.1); return 0;
}
請注意,刪除的函數是隱式內聯的,這一點非常重要。刪除的函數定義必須是函數的第一個聲明。換句話說,以下方法是將函數聲明為已刪除的正確方法:
class C {
public:C(C& a) = delete;
};
但是以下嘗試聲明刪除函數的方法會產生錯誤:
// incorrect syntax of declaring a member function as deleted
class C {
public: C();
}; // Error, the deleted definition of function C must be the first declaration of the function.
C::C() = delete;
最后,明確刪除函數有什么好處?
刪除特殊成員函數提供了一種更簡潔的方法來防止編譯器生成我們不想要的特殊成員函數。(如“禁用拷貝構造函數”示例中所示)。
刪除正常成員函數或非成員函數可防止有問題的類型導致調用非預期函數(如“禁用不需要的參數轉換”示例中所示)。
Big Five,指每個類的拷貝控制,即構造函數、拷貝構造函數、移動構造函數、拷貝賦值函數、移動賦值函數、析構函數。它們默認是 public 且 inline 的。
=default
不能用于 Big Five 之外的常規函數:編譯會報錯,因為其他函數并沒有默認的版本。=delete
可以用于任何函數身上(但好像沒什么意義,不需要某個函數不寫就是了,為什么要寫了再=delete呢),注意類似的=0
只能用于虛函數,這樣會使得該虛函數稱為純虛函數,強迫子類重寫該函數。
alias template (template typedef)
帶參數的別名模板。
template <typename T>
using Vec = std::vector<T, MyAlloc<T>>;
注意這里的 using
關鍵字并不是 C++11 的新東西,但是 using
關鍵字的這種使用方法是C++11之后的新的用來做 alias template 的方法。
在經過了上面的定義之后,以下兩種寫法是等價的:
Vec<int> coll;
// 等價于
std::vector<int, MyAlloc<int>> coll;
如此我們可以方便地使用我們自己的分配器 MyAlloc
創建一個類型可選的 vector 對象。
注意,大家注意到這種用法和我們的宏定義和 typedef
好像有些類似,但是實際上使用 macro 宏定義或 typedef
均無法實現上面的效果。
-
若使用宏定義:
#define Vec<T> template<typename T> std::vector<T, MyAlloc<T>>;
我們知道宏定義就是機械地字符替換,所以在使用時:
Vec<int> coll; // 等價于 template<typename int> std::vector<int, MyAlloc<int>>;
完全不是我們想要的意思。
-
若使用
typedef
也不行,因為typedef
是不接收參數的。至多寫成:
typedef std::vector<int, MyAlloc<int>> Vec;
這當然也不是我們想要的,沒辦法指定變量的類型。
注意 alias template 不能做偏特化或全特化。
type alias (similar to typedef)
using value_type = T;
// 等價于
typedef T value_type;
與上面的 alias template 類似,這里的 using
關鍵字的這種使用方法是C++11之后的新的用來做 type alias 的方法。
using func = void(*)(int, int);
// 等價于
typedef void (*func)(int, int);// 使用func,作為函數指針類
void example(int, int) {}
func fn = example;
后面這個例子中的 func
被定義為一種類型,它是一個函數指針類型。
using關鍵字總結
-
using-directives,如
using namespace std;
-
using-declarations for namespace members,如
using std::cout;
-
using-declarations for class members,如
using _Base::_M_allocate;
-
type alias (since C++11),如:
template <typename T> using Vec = std::vector<T, MyAlloc<T>>;
-
alias template (since C++11),如
using func = void(*)(int, int);
noexcept
void foo() noexcept {// ...
}
程序員保證 foo()
函數不會拋出異常,讓別人/編譯器“放心地”調用該函數。
實際上 noexcept
關鍵字還可以加參數,來表示在…條件下,函數不會拋出異常,上面的 void foo() noexcept ;
就等價于 void foo() noexcept(true);
, 即相當于無條件保證。
而下面:
void swap(Type& x, Type& y) noexcept(noexcept(x.swap(y))) {x.swap(y);
}
則意為在 x.swap(y)
不拋出異常的條件下,本函數不會拋出異常。
補充一下,異常是這樣的,如果 A 調用 B,B 調用 C,而在 C 執行的過程中出現了異常,則先看 C 有沒有寫明異常處理程序,如果有,則處理;如果沒有,則異常傳遞給 B,然后看 B 有沒有對應的異常處理程序,如果有,則處理;如果也沒有,則繼續傳遞給 A。即按照調用順序一層一層地向上傳遞,直到有對應的異常處理程序。如果用戶一直沒有異常處理程序,則執行 std::terminate()
,進而執行 std::abort()
,程序退出。
override
override
關鍵字,標明重寫,應用于虛函數身上。
考慮下面這種情況:
struct Base {virtual void vfunc(float) { }
};struct Derived: Base {// virtual void vfunc(int) { }virtual void vfunc(int) override { }
}
子類 Derived
在繼承父類 Base
之后想要重寫父類的 void vfunc(float)
方法,但是我們知道,要重寫父類方法需要函數簽名完全一致,這里可能由于疏忽大意,將參數類型寫為了 int。這就導致子類的這個函數定義了一個新函數,而非是期望中的對于父類函數的重寫了。而編譯器肯定是不知道你其實是想重寫父類方法的,因為你函數簽名的不一致,就按照一個新方法來處理了。
在 C++11 之后,引入了 override
關鍵字,在你確實想要重寫的函數之后,加上這個關鍵字,編譯器會在你在想重寫但是函數簽名寫錯的時候提醒你,這個被標記為重寫函數的函數實際上并未進行重寫。
final
-
修飾類,使得該類不能被繼承
struct Base final {};struct Derived: Base {}; // Error
Base 類被
final
關鍵字修飾,使得其不能被繼承,下面的 Derived 試圖繼承它,會報錯。 -
修飾虛函數,使得該虛函數不能被重寫
struct Base {virtual void func() final; }struct Derived : Base {void func(); // Error }
Base 類本身沒有被
final
修飾,所以可以被繼承。但是其虛函數func()
被final
關鍵字修飾,故func()
不能被重寫。下面的 Derive 類試圖重寫它,會報錯。
decltype
獲取一個變量/一個對象的類型 (即 tpyeof(a)
)是非常常見的需求,但是在 C++11 之前并沒有直接提供這樣的關鍵字(僅有 typeid 等)。 decltype
可以滿足這一需求,方便地獲得變量 / 對象的類型。
decltype
用來定義一種類型,該類型等同于一個類型表達式的結果。如 decltype(x+y)
定義了 x+y
這個表達式的返回類型。
map<string, float> coll;
decltype(coll)::value_type elem;
在C++11之前只能:
map<string, float>::value_type elem;
這當然不能讓我們在未知變量 / 對象的類型的條件下知道其類型。
decltype
的三種應用場景:
1-用來聲明返回值類型
有時候,函數返回值的類型取決于參數的表達式的執行結果。然而,在C++11之前,沒有 decltype
之前,以下語句是不可能成立的:
template<typename T1, typename T2>
decltype(x+y) add(T1 x, T2 y);
因為上面的返回值的類型使用了尚未引入且不再作用域內的對象。
但是在C++11之后,我們可以通過在函數的參數列表之后聲明一個返回值類型來實現:
template<typename T1, typename T2>
auto add(T1 x, T2 y) -> decltype(x+y);
這與 lambda 表達式聲明返回值的語法相同:
[...](...)mutableoptthrowSpecopt?>retTypeopt{...}[...](...)\ mutable_{opt}\ throwSpec_{opt}->retType_{opt}\{...\} [...](...)?mutableopt??throwSpecopt??>retTypeopt?{...}
2-元編程
元編程是對模板編程的運用。
舉例:
typdef typename decltype(obj)::iterator iType;
// 類似 typedef typename T::iterator iType;
decltype(obj) anotherObj(obj);
3-傳遞lambda的類型
面對lambda,我們手上往往只有對象,沒有類型,要獲得其類型就得借助于 decltype
。
如:
auto cmp = [] (const Person& p1, const Person &p2) {return /* 給出Person類比大小的準則 */
}//...
std::set<Person, decltype(cmp)> coll<cmp>;
我們知道由于 set 是有序容器,所以在將自定義的類構成一個 set 的時候需要給出該類比大小的準則(謂詞),通常是函數、仿函數或者 lambda 表達式。但是這里我們同樣需要指定類型,這就可以用 decltype
來指定。
lambdas
C++11 引入了 lambdas ,允許定義一個單行的函數,可以用作是參數或者局部對象。Lambdas 的引入改變了C++標準庫的使用方式(比如原來的一些仿函數謂詞,現在可直接用)。
基本用法
最簡單的 lambda 函數不接收參數,并做一些簡單的事情,比如這里的打印一句話:
[] { std::cout << "Hello Lambda" << std::endl; }
我們可以直接調用它,就像調用普通函數和函數對象那樣,用 ()
:
[] { std::cout << "Hello Lambda" << std::endl; }();
雖然可以這樣直接低啊用,但是這樣其實沒什么意義,因為你想要打印直接打印就好了,沒必要再繞個圈子,我們通常將 lambda 函數賦值給一個變量,這樣就能像調用普通函數那樣多次調用它:
auto l = [] { std::cout << "Hello Lambda" << std::endl; };
l();
l();
l();
這里 lambda 對象的類型很復雜,通常也沒有必要顯式地寫出來,我們正好用前面介紹過的 C++11 中的 auto
來簡化我們的代碼。如果一定要拿到 lambda 函數對象的類型,參考上面的 decltype
的用法三。
完整形式
lambda 表達式的完整形式:
[...](...)mutableoptthrowSpecopt?>retTypeopt{...}[...]\ (...)\ mutable_{opt}\ throwSpec_{opt}->retType_{opt}\{...\} [...]?(...)?mutableopt??throwSpecopt??>retTypeopt?{...}
- lambda 函數除了少數幾處細節(如沒有默認構造函數、需要加mutable),幾乎完全等同于一個對應的函數對象
[]
稱為 lambda introducer ,其中存放要捕獲的外部變量表,外部變量要注意區分值傳遞和引用傳遞。如果里面放一個等號:[=, &y]
表示接收以值傳遞的形式接收所有的外界變量,不太建議用,要把自己用到的變量寫清楚。()
中存放 lambda 函數的參數列表{}
是 lambda 函數的函數體- 中間的三項(標明 opt 的)都是看情況可有可無的,但是一旦三個中有一個是出現的,那么小括號
()
就必須有;而若三個可選項都沒有,則()
也是可有可無的。 - mutable 指明參數是否可被改變,throwSpec 指明是否可能會拋出異常,retType 指明返回值的類型
- lambda 函數默認是內聯的
舉例:
#include <iostream>int main() {int id = 0;auto f = [id] () mutable {std::cout << "id: " << id << std::endl;++id;};id = 42;f();f();f();std::cout << id << std::endl;
}
輸出:
id: 0
id: 1
id: 2
42
注意:
- 在定義 lambda 函數
f()
時,就已經把 id 以值傳遞的形式傳給函數,因此后面 id 的改變不會影響函數真正被調用時的 id 值 - 不加
mutable
關鍵字會報 id 是只讀變量,不能修改。
varidic template 變參模板詳解
原視頻這里花了很大篇幅來講解變參模板及其應用這個極其重要的新特性,但是考慮到在新手日常編程中的使用并不是太多(而多是出現在大型模板庫的設計中),這里暫時略過,以后再回來補。
Ref:
https://blog.csdn.net/weixin_38339025/article/details/89161324