C++11新特性:
1、auto
2、nullptr
3、for
4、lambda表達式
5、override、final
6、右值引用
7、move構造函數
8、容器初始化
**
> 1. nullptr
**
nullptr 出現的目的是爲了替代 NULL。
在某種意義上來說,傳統 C++ 會把 NULL、0 視爲同一種東西,這取決於編譯器如何定義 NULL,有些編譯器會將 NULL 定義爲 ((void*)0),有些則會直接將其定義爲 0。
C++ 不允許直接將 void * 隱式轉換到其他類型,但如果 NULL 被定義爲 ((void*)0),那麼當編譯char *ch = NULL;時,NULL 只好被定義爲 0。
而這依然會產生問題,將導致了 C++ 中重載特性會發生混亂,考慮:
void foo(char *);
void foo(int);
對於這兩個函數來說,如果 NULL 又被定義爲了 0 那麼 foo(NULL); 這個語句將會去調用 foo(int),從而導致代碼違反直觀。
爲了解決這個問題,C++11 引入了 nullptr 關鍵字,專門用來區分空指針、0。
nullptr 的類型爲 nullptr_t,能夠隱式的轉換爲任何指針或成員指針的類型,也能和他們進行相等或者不等的比較。
當需要使用 NULL 時候,養成直接使用 nullptr的習慣。
- 類型推導
C++11 引入了 auto 和 decltype 這兩個關鍵字實現了類型推導,讓編譯器來操心變量的類型。
**
auto
**
auto 在很早以前就已經進入了 C++,但是他始終作爲一個存儲類型的指示符存在,與 register 並存。在傳統 C++ 中,如果一個變量沒有聲明爲 register 變量,將自動被視爲一個 auto 變量。而隨着 register 被棄用,對 auto 的語義變更也就非常自然了。
使用 auto 進行類型推導的一個最爲常見而且顯著的例子就是迭代器。在以前我們需要這樣來書寫一個迭代器:
for(vector<int>::const_iterator itr = vec.cbegin(); itr != vec.cend(); ++itr)
而有了 auto 之後可以:
// 由於 cbegin() 將返回 vector::const_iterator
// 所以 itr 也應該是 vector::const_iterator 類型
for(auto itr = vec.cbegin(); itr != vec.cend(); ++itr);
一些其他的常見用法:
```cpp
auto i = 5; // i 被推導爲 int
auto arr = new auto(10) // arr 被推導爲 int *
注意:auto 不能用於函數傳參,因此下面的做法是無法通過編譯的(考慮重載的問題,我們應該使用模板):
int add(auto x, auto y);
1
此外,auto 還不能用於推導數組類型:
```cpp
#include <iostream>
int main() {
auto i = 5;
int arr[10] = {0};
auto auto_arr = arr;
auto auto_arr2[10] = arr;
return 0;
}
decltype
decltype 關鍵字是爲了解決 auto 關鍵字只能對變量進行類型推導的缺陷而出現的。它的用法和 sizeof 很相似:
decltype(表達式)
在此過程中,編譯器分析表達式並得到它的類型,卻不實際計算表達式的值。
有時候,我們可能需要計算某個表達式的類型,例如:
auto x = 1;
auto y = 2;
decltype(x+y) z;
拖尾返回類型、auto 與 decltype 配合
你可能會思考,auto 能不能用於推導函數的返回類型。考慮這樣一個例子加法函數的例子,在傳統 C++ 中我們必須這麼寫:
template<typename R, typename T, typename U>
R add(T x, U y) {
return x+y
}
這樣的代碼其實變得很醜陋,因爲程序員在使用這個模板函數的時候,必須明確指出返回類型。但事實上我們並不知道 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 add(T x, U y) -> decltype(x+y) {
return x+y;
}
從 C++14 開始是可以直接讓普通函數具備返回值推導,因此下面的寫法變得合法:
template<typename T, typename U>
auto add(T x, U y) {
return x+y;
}
- 區間迭代
基於範圍的 for 循環
C++11 引入了基於範圍的迭代寫法,我們擁有了能夠寫出像 Python 一樣簡潔的循環語句。
最常用的 std::vector 遍歷將從原來的樣子:
std::vector<int> arr(5, 100);
for(std::vector<int>::iterator i = arr.begin(); i != arr.end(); ++i) {
std::cout << *i << std::endl;
}
變得非常的簡單:
// & 啓用了引用
for(auto &i : arr) {
std::cout << i << std::endl;
}
4. 初始化列表
C++11 提供了統一的語法來初始化任意的對象,例如:
struct A {
int a;
float b;
};
struct B {
B(int _a, float _b): a(_a), b(_b) {}
private:
int a;
float b;
};
A a {1, 1.1}; // 統一的初始化語法
B b {2, 2.2};
C++11 還把初始化列表的概念綁定到了類型上,並將其稱之爲 std::initializer_list,允許構造函數或其他函數像參數一樣使用初始化列表,這就爲類對象的初始化與普通數組和 POD 的初始化方法提供了統一的橋樑,例如:
#include <initializer_list>
class Magic {
public:
Magic(std::initializer_list<int> list) {}
};
Magic magic = {1,2,3,4,5};
std::vector<int> v = {1, 2, 3, 4};
- 模板增強
外部模板
傳統 C++ 中,模板只有在使用時纔會被編譯器實例化。只要在每個編譯單元(文件)中編譯的代碼中遇到了被完整定義的模板,都會實例化。這就產生了重複實例化而導致的編譯時間的增加。並且,我們沒有辦法通知編譯器不要觸發模板實例化。
C++11 引入了外部模板,擴充了原來的強制編譯器在特定位置實例化模板的語法,使得能夠顯式的告訴編譯器何時進行模板的實例化:
template class std::vector<bool>; // 強行實例化
extern template class std::vector<double>; // 不在該編譯文件中實例化模板
尖括號 “>”
在傳統 C++ 的編譯器中,>>一律被當做右移運算符來進行處理。但實際上我們很容易就寫出了嵌套模板的代碼:
std::vector<std::vector<int>> wow;
這在傳統C++編譯器下是不能夠被編譯的,而 C++11 開始,連續的右尖括號將變得合法,並且能夠順利通過編譯。
類型別名模板
在傳統 C++中,typedef 可以爲類型定義一個新的名稱,但是卻沒有辦法爲模板定義一個新的名稱。因爲,模板不是類型。例如:
template< typename T, typename U, int value>
class SuckType {
public:
T a;
U b;
SuckType():a(value),b(value){}
};
template< typename U>
typedef SuckType<std::vector<int>, U, 1> NewType; // 不合法
C++11 使用 using 引入了下面這種形式的寫法,並且同時支持對傳統 typedef 相同的功效:
template <typename T>
using NewType = SuckType<int, T, 1>; // 合法
默認模板參數
我們可能定義了一個加法函數:
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
return x+y
}
但在使用時發現,要使用 add,就必須每次都指定其模板參數的類型。
在 C++11 中提供了一種便利,可以指定模板的默認參數:
template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
- 構造函數
委託構造
C++11 引入了委託構造的概念,這使得構造函數可以在同一個類中一個構造函數調用另一個構造函數,從而達到簡化代碼的目的:
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委託 Base() 構造函數
value2 = 2;
}
};
繼承構造
在繼承體系中,如果派生類想要使用基類的構造函數,需要在構造函數中顯式聲明。
假若基類擁有爲數衆多的不同版本的構造函數,這樣,在派生類中得寫很多對應的“透傳”構造函數。如下:
struct A
{
A(int i) {}
A(double d,int i){}
A(float f,int i,const char* c){}
//...等等系列的構造函數版本
};
struct B:A
{
B(int i):A(i){}
B(double d,int i):A(d,i){}
B(folat f,int i,const char* c):A(f,i,e){}
//......等等好多個和基類構造函數對應的構造函數
};
C++11的繼承構造:
struct A
{
A(int i) {}
A(double d,int i){}
A(float f,int i,const char* c){}
//...等等系列的構造函數版本
};
struct B:A
{
using A::A;
//關於基類各構造函數的繼承一句話搞定
//......
};
如果一個繼承構造函數不被相關的代碼使用,編譯器不會爲之產生真正的函數代碼,這樣比透傳基類各種構造函數更加節省目標代碼空間。
7. Lambda 表達式
Lambda 表達式,實際上就是提供了一個類似匿名函數的特性,而匿名函數則是在需要一個函數,但是又不想費力去命名一個函數的情況下去使用的。
Lambda 表達式的基本語法如下:
[ caputrue ] ( params ) opt -> ret { body; };
1
1) capture是捕獲列表;
2) params是參數表;(選填)
3) opt是函數選項;可以填mutable,exception,attribute(選填)
mutable說明lambda表達式體內的代碼可以修改被捕獲的變量,並且可以訪問被捕獲的對象的non-const方法。
exception說明lambda表達式是否拋出異常以及何種異常。
attribute用來聲明屬性。
4) ret是返回值類型(拖尾返回類型)。(選填)
5) body是函數體。
捕獲列表:lambda表達式的捕獲列表精細控制了lambda表達式能夠訪問的外部變量,以及如何訪問這些變量。
- []不捕獲任何變量。
- [&]捕獲外部作用域中所有變量,並作爲引用在函數體中使用(按引用捕獲)。
- [=]捕獲外部作用域中所有變量,並作爲副本在函數體中使用(按值捕獲)。注意值捕獲的前提是變量可以拷貝,且被捕獲的變量在 lambda 表達式被創建時拷貝,而非調用時才拷貝。如果希望lambda表達式在調用時能即時訪問外部變量,我們應當使用引用方式捕獲。
int a = 0;
auto f = [=] { return a; };
a+=1;
cout << f() << endl; //輸出0
int a = 0;
auto f = [&a] { return a; };
a+=1;
cout << f() <<endl; //輸出1
- [=,&foo]按值捕獲外部作用域中所有變量,並按引用捕獲foo變量。
- [bar]按值捕獲bar變量,同時不捕獲其他變量。
- [this]捕獲當前類中的this指針,讓lambda表達式擁有和當前類成員函數同樣的訪問權限。如果已經使用了&或者=,就默認添加此選項。捕獲this的目的是可以在lamda中使用當前類的成員函數和成員變量。
class A
{
public:
int i_ = 0;
void func(int x,int y){
auto x1 = [] { return i_; }; //error,沒有捕獲外部變量
auto x2 = [=] { return i_ + x + y; }; //OK
auto x3 = [&] { return i_ + x + y; }; //OK
auto x4 = [this] { return i_; }; //OK
auto x5 = [this] { return i_ + x + y; }; //error,沒有捕獲x,y
auto x6 = [this, x, y] { return i_ + x + y; }; //OK
auto x7 = [this] { return i_++; }; //OK
};
int a=0 , b=1;
auto f1 = [] { return a; }; //error,沒有捕獲外部變量
auto f2 = [&] { return a++ }; //OK
auto f3 = [=] { return a; }; //OK
auto f4 = [=] {return a++; }; //error,a是以複製方式捕獲的,無法修改
auto f5 = [a] { return a+b; }; //error,沒有捕獲變量b
auto f6 = [a, &b] { return a + (b++); }; //OK
auto f7 = [=, &b] { return a + (b++); }; //OK
注意f4,雖然按值捕獲的變量值均複製一份存儲在lambda表達式變量中,修改他們也並不會真正影響到外部,但我們卻仍然無法修改它們。如果希望去修改按值捕獲的外部變量,需要顯示指明lambda表達式爲mutable。被mutable修飾的lambda表達式就算沒有參數也要寫明參數列表。
原因:lambda表達式可以說是就地定義仿函數閉包的“語法糖”。它的捕獲列表捕獲住的任何外部變量,最終會變爲閉包類型的成員變量。按照C++標準,lambda表達式的operator()默認是const的,一個const成員函數是無法修改成員變量的值的。而mutable的作用,就在於取消operator()的const。
int a = 0;
auto f1 = [=] { return a++; }; //error
auto f2 = [=] () mutable { return a++; }; //OK
lambda表達式的大致原理:每當你定義一個lambda表達式後,編譯器會自動生成一個匿名類(這個類重載了()運算符),我們稱爲閉包類型(closure type)。那麼在運行時,這個lambda表達式就會返回一個匿名的閉包實例,是一個右值。所以,我們上面的lambda表達式的結果就是一個個閉包。對於複製傳值捕捉方式,類中會相應添加對應類型的非靜態數據成員。在運行時,會用複製的值初始化這些成員變量,從而生成閉包。對於引用捕獲方式,無論是否標記mutable,都可以在lambda表達式中修改捕獲的值。至於閉包類中是否有對應成員,C++標準中給出的答案是:不清楚的,與具體實現有關。
lambda表達式是不能被賦值的:
auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };
a = b; // 非法,lambda無法賦值
auto c = a; // 合法,生成一個副本
閉包類型禁用了賦值操作符,但是沒有禁用複製構造函數,所以你仍然可以用一個lambda表達式去初始化另外一個lambda表達式而產生副本。
在多種捕獲方式中,最好不要使用[=]和[&]默認捕獲所有變量。
默認引用捕獲所有變量,你有很大可能會出現懸掛引用(Dangling references),因爲引用捕獲不會延長引用的變量的生命週期:
std::function<int(int)> add_x(int x)
{
return [&](int a) { return x + a; };
}
上面函數返回了一個lambda表達式,參數x僅是一個臨時變量,函數add_x調用後就被銷燬了,但是返回的lambda表達式卻引用了該變量,當調用這個表達式時,引用的是一個垃圾值,會產生沒有意義的結果。上面這種情況,使用默認傳值方式可以避免懸掛引用問題。
但是採用默認值捕獲所有變量仍然有風險,看下面的例子:
class Filter
{
public:
Filter(int divisorVal):
divisor{divisorVal}
{}
std::function<bool(int)> getFilter()
{
return [=](int value) {return value % divisor == 0; };
}
private:
int divisor;
};
這個類中有一個成員方法,可以返回一個lambda表達式,這個表達式使用了類的數據成員divisor。而且採用默認值方式捕捉所有變量。你可能認爲這個lambda表達式也捕捉了divisor的一份副本,但是實際上並沒有。因爲數據成員divisor對lambda表達式並不可見,你可以用下面的代碼驗證:
// 類的方法,下面無法編譯,因爲divisor並不在lambda捕捉的範圍
std::function<bool(int)> getFilter()
{
return [divisor](int value) {return value % divisor == 0; };
}
原代碼中,lambda表達式實際上捕捉的是this指針的副本,所以原來的代碼等價於:
std::function<bool(int)> getFilter()
{
return [this](int value) {return value % this->divisor == 0; };
}
儘管還是以值方式捕獲,但是捕獲的是指針,其實相當於以引用的方式捕獲了當前類對象,所以lambda表達式的閉包與一個類對象綁定在一起了,這很危險,因爲你仍然有可能在類對象析構後使用這個lambda表達式,那麼類似“懸掛引用”的問題也會產生。所以,採用默認值捕捉所有變量仍然是不安全的,主要是由於指針變量的複製,實際上還是按引用傳值。
lambda表達式可以賦值給對應類型的函數指針。但是使用函數指針並不是那麼方便。所以STL定義在< functional >頭文件提供了一個多態的函數對象封裝std::function,其類似於函數指針。它可以綁定任何類函數對象,只要參數與返回類型相同。如下面的返回一個bool且接收兩個int的函數包裝器:
std::function<bool(int, int)> wrapper = [](int x, int y) { return x < y; };
lambda表達式一個更重要的應用是其可以用於函數的參數,通過這種方式可以實現回調函數。
最常用的是在STL算法中,比如你要統計一個數組中滿足特定條件的元素數量,通過lambda表達式給出條件,傳遞給count_if函數:
int value = 3;
vector<int> v {1, 3, 5, 2, 6, 10};
int count = std::count_if(v.beigin(), v.end(), [value](int x) { return x > value; });
再比如你想生成斐波那契數列,然後保存在數組中,此時你可以使用generate函數,並輔助lambda表達式:
vector<int> v(10);
int a = 0;
int b = 1;
std::generate(v.begin(), v.end(), [&a, &b] { int value = b; b = b + a; a = value; return value; });
// 此時v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}
當需要遍歷容器並對每個元素進行操作時:
std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
int even_count = 0;
for_each(v.begin(), v.end(), [&even_count](int val){
if(!(val & 1)){
++ even_count;
}
});
std::cout << "The number of even is " << even_count << std::endl;
大部分STL算法,可以非常靈活地搭配lambda表達式來實現想要的效果。
**
> 8. 新增容器
**
std::array
std::array 保存在棧內存中,相比堆內存中的 std::vector,我們能夠靈活的訪問這裏面的元素,從而獲得更高的性能。
std::array 會在編譯時創建一個固定大小的數組,std::array 不能夠被隱式的轉換成指針,使用 std::array只需指定其類型和大小即可:
std::array<int, 4> arr= {1,2,3,4};
int len = 4;
std::array<int, len> arr = {1,2,3,4}; // 非法, 數組大小參數必須是常量表達式
當我們開始用上了 std::array 時,難免會遇到要將其兼容 C 風格的接口,這裏有三種做法:
void foo(int *p, int len) {
return;
}
std::array<int 4> arr = {1,2,3,4};
// C 風格接口傳參
// foo(arr, arr.size()); // 非法, 無法隱式轉換
foo(&arr[0], arr.size());
foo(arr.data(), arr.size());
// 使用 `std::sort`
std::sort(arr.begin(), arr.end());
std::forward_list
std::forward_list 是一個列表容器,使用方法和 std::list 基本類似。
和 std::list 的雙向鏈表的實現不同,std::forward_list 使用單向鏈表進行實現,提供了 O(1) 複雜度的元素插入,不支持快速隨機訪問(這也是鏈表的特點),也是標準庫容器中唯一一個不提供 size() 方法的容器。當不需要雙向迭代時,具有比 std::list 更高的空間利用率。
無序容器
C++11 引入了兩組無序容器:
std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset。
無序容器中的元素是不進行排序的,內部通過 Hash 表實現,插入和搜索元素的平均複雜度爲 O(constant)。
元組 std::tuple
元組的使用有三個核心的函數:
std::make_tuple: 構造元組
std::get: 獲得元組某個位置的值
std::tie: 元組拆包
#include <tuple>
#include <iostream>
auto get_student(int id)
{
// 返回類型被推斷爲 std::tuple<double, char, std::string>
if (id == 0)
return std::make_tuple(3.8, 'A', "張三");
if (id == 1)
return std::make_tuple(2.9, 'C', "李四");
if (id == 2)
return std::make_tuple(1.7, 'D', "王五");
return std::make_tuple(0.0, 'D', "null");
// 如果只寫 0 會出現推斷錯誤, 編譯失敗
}
int main()
{
auto student = get_student(0);
std::cout << "ID: 0, "
<< "GPA: " << std::get<0>(student) << ", "
<< "成績: " << std::get<1>(student) << ", "
<< "姓名: " << std::get<2>(student) << '\n';
double gpa;
char grade;
std::string name;
// 元組進行拆包
std::tie(gpa, grade, name) = get_student(1);
std::cout << "ID: 1, "
<< "GPA: " << gpa << ", "
<< "成績: " << grade << ", "
<< "姓名: " << name << '\n';
}
合併兩個元組,可以通過 std::tuple_cat 來實現。
auto new_tuple = std::tuple_cat(get_student(1), std::move(t));
**
9. 正則表達式
**
正則表達式描述了一種字符串匹配的模式。一般使用正則表達式主要是實現下面三個需求:
- 檢查一個串是否包含某種形式的子串;
- 將匹配的子串替換;
- 從某個串中取出符合條件的子串。
C++11 提供的正則表達式庫操作 std::string 對象,對模式 std::regex (本質是 std::basic_regex)進行初始化,通過 std::regex_match 進行匹配,從而產生 std::smatch (本質是 std::match_results 對象)。
我們通過一個簡單的例子來簡單介紹這個庫的使用。考慮下面的正則表達式:
[a-z]+.txt: 在這個正則表達式中, [a-z] 表示匹配一個小寫字母, + 可以使前面的表達式匹配多次,因此 [a-z]+ 能夠匹配一個及以上小寫字母組成的字符串。在正則表達式中一個 . 表示匹配任意字符,而 . 轉義後則表示匹配字符 . ,最後的 txt 表示嚴格匹配 txt 這三個字母。因此這個正則表達式的所要匹配的內容就是文件名爲純小寫字母的文本文件。
std::regex_match 用於匹配字符串和正則表達式,有很多不同的重載形式。最簡單的一個形式就是傳入std::string 以及一個 std::regex 進行匹配,當匹配成功時,會返回 true,否則返回 false。例如:
#include <iostream>
#include <string>
#include <regex>
int main() {
std::string fnames[] = {"foo.txt", "bar.txt", "test", "a0.txt", "AAA.txt"};
// 在 C++ 中 `\` 會被作爲字符串內的轉義符,爲使 `\.` 作爲正則表達式傳遞進去生效,需要對 `\` 進行二次轉義,從而有 `\\.`
std::regex txt_regex("[a-z]+\\.txt");
for (const auto &fname: fnames)
std::cout << fname << ": " << std::regex_match(fname, txt_regex) << std::endl;
}
另一種常用的形式就是依次傳入 std::string/std::smatch/std::regex 三個參數,其中 std::smatch 的本質其實是 std::match_results,在標準庫中, std::smatch 被定義爲了 std::match_results,也就是一個子串迭代器類型的 match_results。使用 std::smatch 可以方便的對匹配的結果進行獲取,例如:
std::regex base_regex("([a-z]+)\\.txt");
std::smatch base_match;
for(const auto &fname: fnames) {
if (std::regex_match(fname, base_match, base_regex)) {
// sub_match 的第一個元素匹配整個字符串
// sub_match 的第二個元素匹配了第一個括號表達式
if (base_match.size() == 2) {
std::string base = base_match[1].str();
std::cout << "sub-match[0]: " << base_match[0].str() << std::endl;
std::cout << fname << " sub-match[1]: " << base << std::endl;
}
}
}
以上兩個代碼段的輸出結果爲:
foo.txt: 1
bar.txt: 1
test: 0
a0.txt: 0
AAA.txt: 0
sub-match[0]: foo.txt
foo.txt sub-match[1]: foo
sub-match[0]: bar.txt
bar.txt sub-match[1]: bar
> 10. 語言級線程支持
std::thread
std::mutex/std::unique_lock
std::future/std::packaged_task
std::condition_variable
代碼編譯需要使用 -pthread 選項
- 右值引用和move語義
先看一個簡單的例子直觀感受下:
string a(x); // line 1
string b(x + y); // line 2
string c(some_function_returning_a_string()); // line 3
如果使用以下拷貝構造函數:
string(const string& that)
{
size_t size = strlen(that.data) + 1;
data = new char[size];
memcpy(data, that.data, size);
}
以上3行中,只有第一行(line 1)的x深度拷貝是有必要的,因爲我們可能會在後邊用到x,x是一個左值(lvalues)。
第二行和第三行的參數則是右值,因爲表達式產生的string對象是匿名對象,之後沒有辦法再使用了。
C++ 11引入了一種新的機制叫做“右值引用”,以便我們通過重載直接使用右值參數。我們所要做的就是寫一個以右值引用爲參數的構造函數:
string(string&& that) // string&& is an rvalue reference to a string
{
data = that.data;
that.data = 0;
}
我們沒有深度拷貝堆內存中的數據,而是僅僅複製了指針,並把源對象的指針置空。事實上,我們“偷取”了屬於源對象的內存數據。由於源對象是一個右值,不會再被使用,因此客戶並不會覺察到源對象被改變了。在這裏,我們並沒有真正的複製,所以我們把這個構造函數叫做“轉移構造函數”(move constructor),他的工作就是把資源從一個對象轉移到另一個對象,而不是複製他們。
有了右值引用,再來看看賦值操作符:
string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}
注意到我們是直接對參數that傳值,所以that會像其他任何對象一樣被初始化,那麼確切的說,that是怎樣被初始化的呢?對於C++ 98,答案是複製構造函數,但是對於C++ 11,編譯器會依據參數是左值還是右值在複製構造函數和轉移構造函數間進行選擇。
如果是a=b,這樣就會調用複製構造函數來初始化that(因爲b是左值),賦值操作符會與新創建的對象交換數據,深度拷貝。這就是copy and swap 慣用法的定義:構造一個副本,與副本交換數據,並讓副本在作用域內自動銷燬。這裏也一樣。
如果是a = x + y,這樣就會調用轉移構造函數來初始化that(因爲x+y是右值),所以這裏沒有深度拷貝,只有高效的數據轉移。相對於參數,that依然是一個獨立的對象,但是他的構造函數是無用的(trivial),因此堆中的數據沒有必要複製,而僅僅是轉移。沒有必要複製他,因爲x+y是右值,再次,從右值指向的對象中轉移是沒有問題的。
總結一下:複製構造函數執行的是深度拷貝,因爲源對象本身必須不能被改變。而轉移構造函數卻可以複製指針,把源對象的指針置空,這種形式下,這是安全的,因爲用戶不可能再使用這個對象了。
下面我們進一步討論右值引用和move語義。
C++98標準庫中提供了一種唯一擁有性的智能指針std::auto_ptr,該類型在C++11中已被廢棄,因爲其“複製”行爲是危險的。
auto_ptr<Shape> a(new Triangle);
auto_ptr<Shape> b(a);
注意b是怎樣使用a進行初始化的,它不復制triangle,而是把triangle的所有權從a傳遞給了b,也可以說成“a 被轉移進了b”或者“triangle被從a轉移到了b”。
auto_ptr 的複製構造函數可能看起來像這樣(簡化):
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
auto_ptr 的危險之處在於看上去應該是複製,但實際上確是轉移。調用被轉移過的auto_ptr 的成員函數將會導致不可預知的後果。所以你必須非常謹慎的使用auto_ptr ,如果他被轉移過。
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
顯然,在持有auto_ptr 對象的a表達式和持有調用函數返回的auto_ptr值類型的make_triangle()表達式之間一定有一些潛在的區別,每調用一次後者就會創建一個新的auto_ptr對象。這裏a 其實就是一個左值(lvalue)的例子,而make_triangle()就是右值(rvalue)的例子。
轉移像a這樣的左值是非常危險的,因爲我們可能調用a的成員函數,這會導致不可預知的行爲。另一方面,轉移像make_triangle()這樣的右值卻是非常安全的,因爲複製構造函數之後,我們不能再使用這個臨時對象了,因爲這個轉移後的臨時對象會在下一行之前銷燬掉。
我們現在知道轉移左值是十分危險的,但是轉移右值卻是很安全的。如果C++能從語言級別支持區分左值和右值參數,我就可以完全杜絕對左值轉移,或者把轉移左值在調用的時候暴露出來,以使我們不會不經意的轉移左值。
C++ 11對這個問題的答案是右值引用。右值引用是針對右值的新的引用類型,語法是X&&。以前的老的引用類型X& 現在被稱作左值引用。
使用右值引用X&&作爲參數的最有用的函數之一就是轉移構造函數X::X(X&& source),它的主要作用是把源對象的本地資源轉移給當前對象。
C++ 11中,std::auto_ptr< T >已經被std::unique_ptr< T >所取代,後者就是利用的右值引用。
其轉移構造函數:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
這個轉移構造函數跟auto_ptr中複製構造函數做的事情一樣,但是它卻只能接受右值作爲參數。
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
第二行不能編譯通過,因爲a是左值,但是參數unique_ptr&& source只能接受右值,這正是我們所需要的,杜絕危險的隱式轉移。第三行編譯沒有問題,因爲make_triangle()是右值,轉移構造函數會將臨時對象的所有權轉移給對象c,這正是我們需要的。
轉移左值
有時候,我們可能想轉移左值,也就是說,有時候我們想讓編譯器把左值當作右值對待,以便能使用轉移構造函數,即便這有點不安全。出於這個目的,C++ 11在標準庫的頭文件< utility >中提供了一個模板函數std::move。實際上,std::move僅僅是簡單地將左值轉換爲右值,它本身並沒有轉移任何東西。它僅僅是讓對象可以轉移。
以下是如何正確的轉移左值:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
請注意,第三行之後,a不再擁有Triangle對象。不過這沒有關係,因爲通過明確的寫出std::move(a),我們很清楚我們的意圖:親愛的轉移構造函數,你可以對a做任何想要做的事情來初始化c;我不再需要a了,對於a,您請自便。
當然,如果你在使用了mova(a)之後,還繼續使用a,那無疑是搬起石頭砸自己的腳,還是會導致嚴重的運行錯誤。
總之,std::move(some_lvalue)將左值轉換爲右值(可以理解爲一種類型轉換),使接下來的轉移成爲可能。
一個例子:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
上面的parameter,其類型是一個右值引用,只能說明parameter是指向右值的引用,而parameter本身是個左值。(Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.)
因此以上對parameter的轉移是不允許的,需要使用std::move來顯示轉換成右值。