在C++11新標準中,語言本身和標準庫都增加了很多新內容,本文只涉及了一些皮毛。不過我相信這些新特性當中有一些,應該成爲所有C++開發者的常規裝備。你也許看到過許多類似介紹各種C++11特性的文章。下面是我總結的,C++開發者都需要學習和使用的C++11新特性。
auto
在C++11之前,auto關鍵字用來指定存儲期。在新標準中,它的功能變爲類型推斷。auto現在成了一個類型的佔位符,通知編譯器去根據初始化代碼推斷所聲明變量的真實類型。各種作用域內聲明變量都可以用到它。例如,名空間中,程序塊中,或是for循環的初始化語句中。
1
2
3
|
auto
i = 42; //
i is an int auto
l = 42LL; //
l is an long long auto
p = new foo(); //
p is a foo* |
使用auto通常意味着更短的代碼(除非你所用類型是int,它會比auto少一個字母)。試想一下當你遍歷STL容器時需要聲明的那些迭代器(iterator)。現在不需要去聲明那些typedef就可以得到簡潔的代碼了。
1
2
3
4
|
std::map<std::string,
std::vector< int >>
map; for (auto
it = begin(map); it != end(map); ++it) { } |
需要注意的是,auto不能用來聲明函數的返回值。但如果函數有一個尾隨的返回類型時,auto是可以出現在函數聲明中返回值位置。這種情況下,auto並不是告訴編譯器去推斷返回類型,而是指引編譯器去函數的末端尋找返回值類型。在下面這個例子中,函數的返回值類型就是operator+操作符作用在T1、T2類型變量上的返回值類型。
1
2
3
4
5
6
|
template < typename T1, typename T2> auto
compose(T1 t1, T2 t2) -> decltype(t1 + t2) { return t1+t2; } auto
v = compose(2, 3.14); //
v's type is double |
nullptr
以前都是用0來表示空指針的,但由於0可以被隱式類型轉換爲整形,這就會存在一些問題。關鍵字nullptr是std::nullptr_t類型的值,用來指代空指針。nullptr和任何指針類型以及類成員指針類型的空值之間可以發生隱式類型轉換,同樣也可以隱式轉換爲bool型(取值爲false)。但是不存在到整形的隱式類型轉換。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
void foo( int *
p) {} void bar(std::shared_ptr< int >
p) {} int *
p1 = NULL; int *
p2 = nullptr; if (p1
== p2) { } foo(nullptr); bar(nullptr); bool f
= nullptr; int i
= nullptr; //
error: A native nullptr can only be converted to bool or, using reinterpret_cast, to an integral type |
爲了向前兼容,0仍然是個合法的空指針值。
Range-based for loops (基於範圍的for循環)
爲了在遍歷容器時支持”foreach”用法,C++11擴展了for語句的語法。用這個新的寫法,可以遍歷C類型的數組、初始化列表以及任何重載了非成員的begin()和end()函數的類型。
如果你只是想對集合或數組的每個元素做一些操作,而不關心下標、迭代器位置或者元素個數,那麼這種foreach的for循環將會非常有用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
std::map<std::string,
std::vector< int >>
map; std::vector< int >
v; v.push_back(1); v.push_back(2); v.push_back(3); map[ "one" ]
= v; for ( const auto&
kvp : map) { std::cout
<< kvp.first << std::endl; for (auto
v : kvp.second) { std::cout
<< v << std::endl; } } int arr[]
= {1,2,3,4,5}; for ( int &
e : arr) { e
= e*e; } |
Override和final
我總覺得 C++中虛函數的設計很差勁,因爲時至今日仍然沒有一個強制的機制來標識虛函數會在派生類裏被改寫。vitual關鍵字是可選的,這使得閱讀代碼變得很費勁。因爲可能需要追溯到繼承體系的源頭才能確定某個方法是否是虛函數。爲了增加可讀性,我總是在派生類裏也寫上virtual關鍵字,並且也鼓勵大家都這麼做。即使這樣,仍然會產生一些微妙的錯誤。看下面這個例子:
1
2
3
4
5
6
7
8
9
10
11
|
class B { public : virtual void f( short )
{std::cout << "B::f" <<
std::endl;} }; class D
: public B { public : virtual void f( int )
{std::cout << "D::f" <<
std::endl;} }; |
D::f 按理應當重寫 B::f。然而二者的聲明是不同的,一個參數是short,另一個是int。因此D::f(原文爲B::f,可能是作者筆誤——譯者注)只是擁有同樣名字的另一個函數(重載)而不是重寫。當你通過B類型的指針調用f()可能會期望打印出D::f,但實際上則會打出 B::f 。
另一個很微妙的錯誤情況:參數相同,但是基類的函數是const的,派生類的函數卻不是。
1
2
3
4
5
6
7
8
9
10
11
|
class B { public : virtual void f( int ) const {std::cout
<< "B::f
"
<< std::endl;} }; class D
: public B { public : virtual void f( int )
{std::cout << "D::f" <<
std::endl;} }; |
同樣,這兩個函數是重載而不是重寫,所以你通過B類型指針調用f()將打印B::f,而不是D::f。
幸運的是,現在有一種方式能描述你的意圖。新標準加入了兩個新的標識符(不是關鍵字)::
- override,表示函數應當重寫基類中的虛函數。
- final,表示派生類不應當重寫這個虛函數。
第一個的例子如下:
1
2
3
4
5
6
7
8
9
10
11
|
class B { public : virtual void f( short )
{std::cout << "B::f" <<
std::endl;} }; class D
: public B { public : virtual void f( int )
override {std::cout << "D::f" <<
std::endl;} }; |
現在這將觸發一個編譯錯誤(後面那個例子,如果也寫上override標識,會得到相同的錯誤提示):
1
|
'D::f'
: method with override specifier 'override' did not override any base class methods |
另一方面,如果你希望函數不要再被派生類進一步重寫,你可以把它標識爲final。可以在基類或任何派生類中使用final。在派生類中,可以同時使用override和final標識。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class B { public : virtual void f( int )
{std::cout << "B::f" <<
std::endl;} }; class D
: public B { public : virtual void f( int )
override final {std::cout << "D::f" <<
std::endl;} }; class F
: public D { public : virtual void f( int )
override {std::cout << "F::f" <<
std::endl;} }; |
被標記成final的函數將不能再被F::f重寫。
Strongly-typed enums 強類型枚舉
傳統的C++枚舉類型存在一些缺陷:它們會將枚舉常量暴露在外層作用域中(這可能導致名字衝突,如果同一個作用域中存在兩個不同的枚舉類型,但是具有相同的枚舉常量就會衝突),而且它們會被隱式轉換爲整形,無法擁有特定的用戶定義類型。
在C++11中通過引入了一個稱爲強類型枚舉的新類型,修正了這種情況。強類型枚舉由關鍵字enum class標識。它不會將枚舉常量暴露到外層作用域中,也不會隱式轉換爲整形,並且擁有用戶指定的特定類型(傳統枚舉也增加了這個性質)。
1
2
|
enum class Options
{None, One, All}; Options
o = Options::All; |
Smart Pointers 智能指針
已經有成千上萬的文章討論這個問題了,所以我只想說:現在能使用的,帶引用計數,並且能自動釋放內存的智能指針包括以下幾種:
- unique_ptr: 如果內存資源的所有權不需要共享,就應當使用這個(它沒有拷貝構造函數),但是它可以轉讓給另一個unique_ptr(存在move構造函數)。
- shared_ptr: 如果內存資源需要共享,那麼使用這個(所以叫這個名字)。
- weak_ptr: 持有被shared_ptr所管理對象的引用,但是不會改變引用計數值。它被用來打破依賴循環(想象在一個tree結構中,父節點通過一個共享所有權的引用(chared_ptr)引用子節點,同時子節點又必須持有父節點的引用。如果這第二個引用也共享所有權,就會導致一個循環,最終兩個節點內存都無法釋放)。
另一方面,auto_ptr已經被廢棄,不會再使用了。
什麼時候使用unique_ptr,什麼時候使用shared_ptr取決於對所有權的需求,我建議閱讀以下的討論:http://stackoverflow.com/questions/15648844/using-smart-pointers-for-class-members
以下第一個例子使用了unique_ptr。如果你想把對象所有權轉移給另一個unique_ptr,需要使用std::move(我會在最後幾段討論這個函數)。在所有權轉移後,交出所有權的智能指針將爲空,get()函數將返回nullptr。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void foo( int *
p) { std::cout
<< *p << std::endl; } std::unique_ptr< int >
p1( new int (42)); std::unique_ptr< int >
p2 = std::move(p1); //
transfer ownership if (p1) foo(p1.get()); (*p2)++; if (p2) foo(p2.get()); |
第二個例子展示了shared_ptr。用法相似,但語義不同,此時所有權是共享的。
1
2
3
4
5
6
7
8
9
10
11
12
|
void foo( int *
p) { } void bar(std::shared_ptr< int >
p) { ++(*p); } std::shared_ptr< int >
p1( new int (42)); std::shared_ptr< int >
p2 = p1; bar(p1); foo(p2.get()); |
第一個聲明和以下這行是等價的:
1
|
auto
p3 = std::make_shared< int >(42); |
make_shared<T>是一個非成員函數,使用它的好處是可以一次性分配共享對象和智能指針自身的內存。而顯示地使用shared_ptr構造函數來構造則至少需要兩次內存分配。除了會產生額外的開銷,還可能會導致內存泄漏。在下面這個例子中,如果seed()拋出一個錯誤就會產生內存泄漏。
1
2
3
4
5
|
void foo(std::shared_ptr< int >
p, int init) { *p
= init; } foo(std::shared_ptr< int >( new int (42)),
seed()); |
如果使用make_shared就不會有這個問題了。第三個例子展示了weak_ptr。注意,你必須調用lock()來獲得被引用對象的shared_ptr,通過它才能訪問這個對象。
1
2
3
4
5
6
7
8
9
10
11
12
|
auto
p = std::make_shared< int >(42); std::weak_ptr< int >
wp = p; { auto
sp = wp.lock(); std::cout
<< *sp << std::endl; } p.reset(); if (wp.expired()) std::cout
<< "expired" <<
std::endl; |
如果你試圖鎖定(lock)一個過期(指被弱引用對象已經被釋放)的weak_ptr,那你將獲得一個空的shared_ptr.
Lambdas
匿名函數(也叫lambda)已經加入到C++中,並很快異軍突起。這個從函數式編程中借來的強大特性,使很多其他特性以及類庫得以實現。你可以在任何使用函數對象或者函子(functor)或std::function的地方使用lambda。你可以從這裏(http://msdn.microsoft.com/en-us/library/dd293603.aspx)找到語法說明。
1
2
3
4
5
6
7
8
9
10
11
|
std::vector< int >
v; v.push_back(1); v.push_back(2); v.push_back(3); std::for_each(std::begin(v),
std::end(v), []( int n)
{std::cout << n << std::endl;}); auto
is_odd = []( int n)
{ return n%2==1;}; auto
pos = std::find_if(std::begin(v), std::end(v), is_odd); if (pos
!= std::end(v)) std::cout
<< *pos << std::endl; |
更復雜的是遞歸lambda。考慮一個實現Fibonacci函數的lambda。如果你試圖用auto來聲明,就會得到一個編譯錯誤。
1
|
auto
fib = [&fib]( int n)
{ return n
< 2 ? 1 : fib(n-1) + fib(n-2);}; |
1
2
3
4
|
error
C3533: 'auto &': a parameter cannot have a type that contains 'auto' error
C3531: 'fib': a symbol whose type contains 'auto' must have an initializer error
C3536: 'fib': cannot be used before it is initialized error
C2064: term does not evaluate to a function taking 1 arguments |
問題出在auto意味着對象類型由初始表達式決定,然而初始表達式又包含了對其自身的引用,因此要求先知道它的類型,這就導致了無窮遞歸。解決問題的關鍵就是打破這種循環依賴,用std::function顯式的指定函數類型:
1
|
std::function< int ( int )>
lfib = [&lfib]( int n)
{ return n
< 2 ? 1 : lfib(n-1) + lfib(n-2);}; |
非成員begin()和end()
也許你注意到了,我在前面的例子中已經用到了非成員begin()和end()函數。他們是新加入標準庫的,除了能提高了代碼一致性,還有助於更多地使用泛型編程。它們和所有的STL容器兼容。更重要的是,他們是可重載的。所以它們可以被擴展到支持任何類型。對C類型數組的重載已經包含在標準庫中了。
我們還用上一個例子中的代碼來說明,在這個例子中我打印了一個數組然後查找它的第一個偶數元素。如果std::vector被替換成C類型數組。代碼可能看起來是這樣的:
1
2
3
4
5
6
7
8
9
|
int arr[]
= {1,2,3}; std::for_each(&arr[0],
&arr[0]+ sizeof (arr)/ sizeof (arr[0]),
[]( int n)
{std::cout << n << std::endl;}); auto
is_odd = []( int n)
{ return n%2==1;}; auto
begin = &arr[0]; auto
end = &arr[0]+ sizeof (arr)/ sizeof (arr[0]); auto
pos = std::find_if(begin, end, is_odd); if (pos
!= end) std::cout
<< *pos << std::endl; |
如果使用非成員的begin()和end()來實現,就會是以下這樣的:
1
2
3
4
5
6
7
|
int arr[]
= {1,2,3}; std::for_each(std::begin(arr),
std::end(arr), []( int n)
{std::cout << n << std::endl;}); auto
is_odd = []( int n)
{ return n%2==1;}; auto
pos = std::find_if(std::begin(arr), std::end(arr), is_odd); if (pos
!= std::end(arr)) std::cout
<< *pos << std::endl; |
這基本上和使用std::vecto的代碼是完全一樣的。這就意味着我們可以寫一個泛型函數處理所有支持begin()和end()的類型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
template < typename Iterator> void bar(Iterator
begin, Iterator end) { std::for_each(begin,
end, []( int n)
{std::cout << n << std::endl;}); auto
is_odd = []( int n)
{ return n%2==1;}; auto
pos = std::find_if(begin, end, is_odd); if (pos
!= end) std::cout
<< *pos << std::endl; } template < typename C> void foo(C
c) { bar(std::begin(c),
std::end(c)); } template < typename T, size_t N> void foo(T(&arr)[N]) { bar(std::begin(arr),
std::end(arr)); } int arr[]
= {1,2,3}; foo(arr); std::vector< int >
v; v.push_back(1); v.push_back(2); v.push_back(3); foo(v); |
static_assert和 type traits
static_assert提供一個編譯時的斷言檢查。如果斷言爲真,什麼也不會發生。如果斷言爲假,編譯器會打印一個特殊的錯誤信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
template < typename T, size_t Size> class Vector { static_assert(Size
< 3, "Size
is too small" ); T
_points[Size]; }; int main() { Vector< int ,
16> a1; Vector< double ,
2> a2; return 0; } |
1
2
3
4
5
6
7
|
error
C2338: Size is too small see
reference to class template instantiation 'Vector<T,Size>' being compiled with [ T=double, Size=2 ] |
static_assert和type traits一起使用能發揮更大的威力。type traits是一些class,在編譯時提供關於類型的信息。在頭文件<type_traits>中可以找到它們。這個頭文件中有好幾種class: helper class,用來產生編譯時常量。type traits class,用來在編譯時獲取類型信息,還有就是type transformation class,他們可以將已存在的類型變換爲新的類型。
下面這段代碼原本期望只做用於整數類型。
1
2
3
4
5
|
template < typename T1, typename T2> auto
add(T1 t1, T2 t2) -> decltype(t1 + t2) { return t1
+ t2; } |
但是如果有人寫出如下代碼,編譯器並不會報錯
1
2
|
std::cout
<< add(1, 3.14) << std::endl; std::cout
<< add( "one" ,
2) << std::endl; |
程序會打印出4.14和”e”。但是如果我們加上編譯時斷言,那麼以上兩行將產生編譯錯誤。
1
2
3
4
5
6
7
8
|
template < typename T1, typename T2> auto
add(T1 t1, T2 t2) -> decltype(t1 + t2) { static_assert(std::is_integral<T1>::value, "Type
T1 must be integral" ); static_assert(std::is_integral<T2>::value, "Type
T2 must be integral" ); return t1
+ t2; } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
error
C2338: Type T2 must be integral see
reference to function template instantiation 'T2 add<int,double>(T1,T2)' being compiled with [ T2=double, T1=int ] error
C2338: Type T1 must be integral see
reference to function template instantiation 'T1 add<const char*,int>(T1,T2)' being compiled with [ T1=const
char *, T2=int ] |
Move semantics (Move語義)
這是C++11中所涵蓋的另一個重要話題。就這個話題可以寫出一系列文章,僅用一個段落來說明顯然是不夠的。因此在這裏我不會過多的深入細節,如果你還不是很熟悉這個話題,我鼓勵你去閱讀更多地資料。
C++11加入了右值引用(rvalue reference)的概念(用&&標識),用來區分對左值和右值的引用。左值就是一個有名字的對象,而右值則是一個無名對象(臨時對象)。move語義允許修改右值(以前右值被看作是不可修改的,等同於const T&類型)。
C++的class或者struct以前都有一些隱含的成員函數:默認構造函數(僅當沒有顯示定義任何其他構造函數時才存在),拷貝構造函數,析構函數還有拷貝賦值操作符。拷貝構造函數和拷貝賦值操作符提供bit-wise的拷貝(淺拷貝),也就是逐個bit拷貝對象。也就是說,如果你有一個類包含指向其他對象的指針,拷貝時只會拷貝指針的值而不會管指向的對象。在某些情況下這種做法是沒問題的,但在很多情況下,實際上你需要的是深拷貝,也就是說你希望拷貝指針所指向的對象。而不是拷貝指針的值。這種情況下,你需要顯示地提供拷貝構造函數與拷貝賦值操作符來進行深拷貝。
如果你用來初始化或拷貝的源對象是個右值(臨時對象)會怎麼樣呢?你仍然需要拷貝它的值,但隨後很快右值就會被釋放。這意味着產生了額外的操作開銷,包括原本並不需要的空間分配以及內存拷貝。
現在說說move constructor和move assignment operator。這兩個函數接收T&&類型的參數,也就是一個右值。在這種情況下,它們可以修改右值對象,例如“偷走”它們內部指針所指向的對象。舉個例子,一個容器的實現(例如vector或者queue)可能包含一個指向元素數組的指針。當用一個臨時對象初始化一個對象時,我們不需要分配另一個數組,從臨時對象中把值複製過來,然後在臨時對象析構時釋放它的內存。我們只需要將指向數組內存的指針值複製過來,由此節約了一次內存分配,一次元數組的複製以及後來的內存釋放。
以下代碼實現了一個簡易的buffer。這個buffer有一個成員記錄buffer名稱(爲了便於以下的說明),一個指針(封裝在unique_ptr中)指向元素爲T類型的數組,還有一個記錄數組長度的變量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
template < typename T> class Buffer { std::string
_name; size_t _size; std::unique_ptr<T[]>
_buffer; public : //
default constructor Buffer(): _size(16), _buffer( new T[16]) {} //
constructor Buffer( const std::string&
name, size_t size): _name(name), _size(size), _buffer( new T[size]) {} //
copy constructor Buffer( const Buffer&
copy): _name(copy._name), _size(copy._size), _buffer( new T[copy._size]) { T*
source = copy._buffer.get(); T*
dest = _buffer.get(); std::copy(source,
source + copy._size, dest); } //
copy assignment operator Buffer&
operator=( const Buffer&
copy) { if ( this !=
©) { _name
= copy._name; if (_size
!= copy._size) { _buffer
= nullptr; _size
= copy._size; _buffer
= _size > 0 > new T[_size]
: nullptr; } T*
source = copy._buffer.get(); T*
dest = _buffer.get(); std::copy(source,
source + copy._size, dest); } return * this ; } //
move constructor Buffer(Buffer&&
temp): _name(std::move(temp._name)), _size(temp._size), _buffer(std::move(temp._buffer)) { temp._buffer
= nullptr; temp._size
= 0; } //
move assignment operator Buffer&
operator=(Buffer&& temp) { assert ( this !=
&temp); //
assert if this is not a temporary _buffer
= nullptr; _size
= temp._size; _buffer
= std::move(temp._buffer); _name
= std::move(temp._name); temp._buffer
= nullptr; temp._size
= 0; return * this ; } }; template < typename T> Buffer<T>
getBuffer( const std::string&
name) { Buffer<T>
b(name, 128); return b; } int main() { Buffer< int >
b1; Buffer< int >
b2( "buf2" ,
64); Buffer< int >
b3 = b2; Buffer< int >
b4 = getBuffer< int >( "buf4" ); b1
= getBuffer< int >( "buf5" ); return 0; } |
默認的copy constructor以及copy assignment operator大家應該很熟悉了。C++11中新增的是move constructor以及move assignment operator,這兩個函數根據上文所描述的move語義實現。如果你運行這段代碼,你就會發現b4構造時,move constructor會被調用。同樣,對b1賦值時,move assignment operator會被調用。原因就在於getBuffer()的返回值是一個臨時對象——也就是右值。
你也許注意到了,move constuctor中當我們初始化變量name和指向buffer的指針時,我們使用了std::move。name實際上是一個string,std::string實現了move語義。std::unique_ptr也一樣。但是如果我們寫_name(temp._name),那麼copy constructor將會被調用。不過對於_buffer來說不能這麼寫,因爲std::unique_ptr沒有copy constructor。但爲什麼std::string的move constructor此時沒有被調到呢?這是因爲雖然我們使用一個右值調用了Buffer的move constructor,但在這個構造函數內,它實際上是個左值。爲什麼?因爲它是有名字的——“temp”。一個有名字的對象就是左值。爲了再把它變爲右值(以便調用move constructor)必須使用std::move。這個函數僅僅是把一個左值引用變爲一個右值引用。
更新:雖然這個例子是爲了說明如何實現move constructor以及move assignment operator,但具體的實現方式並不是唯一的。在本文的回覆中Member 7805758同學提供了另一種可能的實現。爲了方便查看,我把它也列在下面:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
template < typename T> class Buffer { std::string
_name; size_t _size; std::unique_ptr<T[]>
_buffer; public : //
constructor Buffer( const std::string&
name = "" , size_t size
= 16): _name(name), _size(size), _buffer(size? new T[size]
: nullptr) {} //
copy constructor Buffer( const Buffer&
copy): _name(copy._name), _size(copy._size), _buffer(copy._size? new T[copy._size]
: nullptr) { T*
source = copy._buffer.get(); T*
dest = _buffer.get(); std::copy(source,
source + copy._size, dest); } //
copy assignment operator Buffer&
operator=(Buffer copy) { swap(* this ,
copy); return * this ; } //
move constructor Buffer(Buffer&&
temp):Buffer() { swap(* this ,
temp); } friend void swap(Buffer&
first, Buffer& second) noexcept { using std::swap; swap(first._name
, second._name); swap(first._size
, second._size); swap(first._buffer,
second._buffer); } }; |
結論
關於C++11還有很多要說的。本文只是各種入門介紹中的一個。本文展示了一系列C++開發者應當使用的核心語言特性與標準庫函數。然而我建議你能更加深入地學習,至少也要再看看本文所介紹的特性中的部分。