1. 程式人生 > >C++14 常用新特性總結

C++14 常用新特性總結

1. 返回值型別推導(Return type deduction)

為什麼返回型別推導對於C++程式來說是錦上添花的。首先,有時候你必須返回一個非常複雜的型別,比如在對標準庫容器進行搜尋的時候返回一個迭代器。auto返回型別使得函式更加易讀,易寫。其次,這個原因可能不是那麼明顯,使用auto返回型別能夠增強你的重構能力。舉個例子,考慮下面的程式碼:

#include <iostream>
#include <vector>
#include <string>

struct record {
std::string name;
int id;
};

auto find_id(const std::vector<record> &people,
const std::string &name)
{
auto match_name = [&name](const record& r) -> bool {
return r.name == name;
};
auto ii = find_if(people.begin(), people.end(), match_name );
if (ii == people.end())
return -1;
else
return ii->id;
}

int main()
{
std::vector<record> roster = { {"mark",1},
{"bill",2},
{"ted",3}};
std::cout << find_id(roster,"bill") << "\n";
std::cout << find_id(roster,"ron") << "\n";
}

在這個例子中,使find_id返回auto比返回int並沒有使我節省多少腦力。但是考慮一下,如果我決定重構record結構體,會發生生麼。這次我不在record物件中使用一個整型來唯一標記一個人了,而是使用一個新的GUID型別:

struct record {
	std::string name;
	GUID id;
};

對record物件所做的這個改變會導致一系列的連鎖反應,比如函式的返回型別會發生變化。但是如果我對函式返回值使用自動型別推導,編譯器會默默的應對這些變化。任何在大型工程上進行開發的C++程式設計師都會熟悉這個問題。對單一資料結構的修改會導致無休止的對原始碼的迭代,對變數,引數以及返回型別的修改。auto的廣泛使用能夠消減很大一部分這樣的工作。

使用auto作為返回值的立竿見影的效果是reality of it's doppelganger, decltype(auto),還有有了型別推導的規則。現在你可以使用它來自動獲取型別資訊了,如下面的程式碼片段:

template<typename Container>
	struct finder {
	static decltype(Container::find) finder1 = Container::find;
	static decltype(auto) finder2 = Container::find;
};

2. 泛型lambdas

另外一個auto悄悄潛伏的地方是在lambda引數的定義中。使用auto型別宣告定義lambda引數同建立模板函式基本相當。lambda會基於引數推導型別來進行特定的例項化。

這對建立可在不同上下文中重用的lambdas來說是很方便的。在下面的簡單例子中,我建立了一個lambdas用做標準庫函式的謂詞(predicate)。在C++11的世界裡,我分別為整型加法,字串加法顯示的例項化了一個lambda。

有了泛型lambda,我能用其定義單一的lambda。雖然它的語法不包括關鍵字template,這仍然是對C++泛型程式設計的進一步擴充套件:

#include <iostream>
#include <vector>
#include <string>
#include <numeric>

int main()
{
	std::vector<int> ivec = { 1, 2, 3, 4};
	std::vector<std::string> svec = { "red",
	"green",
	"blue" };
	auto adder = [](auto op1, auto op2){ return op1 + op2; };
	std::cout << "int result : "
	<< std::accumulate(ivec.begin(),
	ivec.end(),
	0,
	adder )
	<< "\n";
	std::cout << "string result : "
	<< std::accumulate(svec.begin(),
	svec.end(),
	std::string(""),
	adder )
	<< "\n";
	return 0;
}

產生如下結果:

int result : 10
string result : redgreenblue

即使你對匿名inline lambdas進行例項化,正如如前面討論的,泛型引數仍然是有用的。當你的資料結構發生了變化或者APIs中的函式被修改了,對泛型lambdas進行調整時只需要重新編譯就可以了,不需要重新實現:

std::cout << "string result : "
<< std::accumulate(svec.begin(),
svec.end(),
std::string(""),
[](auto op1,auto op2){ return op1+op2; } )
<< "\n";

3. 被初始化的lambdas捕獲(Initialized lambda captures)

在C++11中我們必須開始適應lambda捕獲特化(lambda capture specification)的概念。這種宣告會在建立閉包(closure)的時候對編譯器進行引導:lambda定義了一個函式的例項,還有定義在lambda作用域之外的繫結在函式上的變數。

在早期的推導返回型別的例子中,我實現了捕獲單個變數名字的一個lambda定義,用做謂詞中搜索字串的源:

auto match_name = [&name](const record& r) -> bool {
	return r.name == name;
};
	auto ii = find_if(people.begin(), people.end(), match_name );

 這種特殊的捕獲使lambda獲取了按引用訪問變數的許可權。捕獲也能按值來執行,在兩種情況中,變數的使用方式都會符合C++的直覺。按值捕獲意味著lambda在本地變數的拷貝上進行操作,按引用捕獲意味著lambda在外圍作用域的變數本身進行操作。


這些都很好,但也會有伴隨而來的一些侷限性。我想委員會感覺需要處理的一件事情是使用move-only語義來初始化捕獲變數。

這意味著什麼?如果我們認為lambda即將成為引數的接收器,我們想使用move語義捕獲外部變數。舉個例子,考慮如何使lambda接收一個move-only unique_ptr引數。第一個嘗試是按值捕獲,失敗了:

std::unique_ptr<int> p(new int);
*p = 11;
auto y = [p]() { std::cout << "inside: " << *p << "\n";};

這會生成一個編譯錯誤因為unique_ptr沒有拷貝建構函式——它所想的就是禁止拷貝。

將其改為按引用捕獲就能編譯通過,但是沒有達到預期效果:也就是通過將值move到本地的拷貝來接收引數。最後你可以通過先建立本地變數然後在捕獲的引用上呼叫std::move()來完成,但是效率不高。

通過對捕獲語句語法進行修改可以修復這個問題。現在我們不是隻宣告一個捕獲變數,我們也能對其初始化。看下面的例子:auto y = [&r = x, x = x+1]()->int {...}

auto y = [&r = x, x = x+1]()->int {...}

 上面的程式碼捕獲了x的拷貝,同時為x增加了1。這個例子很容易理解,但是我不確定它是否為接收move-only變數捕獲了這種新語法的值。使用這個新語法的例子如下:

#include <memory>
#include <iostream>

int main()
{
	std::unique_ptr<int> p(new int);
	int x = 5;
	*p = 11;
	auto y = [p=std::move(p)]() { std::cout << "inside: " << *p << "\n";};
	y();
	std::cout << "outside: " << *p << "\n";
	return 0;
}

在這個例子中,捕獲的值p用move語義來初始化,在沒有宣告本地變數的情況下有效的接收了指標:
inside: 11
Segmentation fault (core dumped)

這個令人討厭的結果正是你想要的——在p被捕獲並且move到lambda中後,程式碼對p進行了解引用。

4. [[棄用的]][[deprecated]]屬性

初次看到deprecated 屬性是在java中,我承認有點嫉妒。對大多數程式設計師來說程式碼腐爛(rot)是一個巨大的問題。(曾經鼓勵刪除程式碼?但我從來沒有這麼做過)。這個新屬性提供了攻克這個問題的系統級別的方法。

用起來很簡單,將標籤【[[deprecated]]放在宣告之前就可以了,宣告可以為類,變數,函式或其他東西。結果像下面這個樣子:

[[deprecated]] flaky {};

當你的程式使用了一個deprecated實體,原本需要編譯器做出反應,現在留給了程式碼實現者。很清楚大多數人希望能夠看到某種警告,也能隨手把這種warning關掉。舉個例子,clang3.4在例項化一個deprecated類的時候會發出以下警告:
dep.cpp:14:3: warning: 'flaky' is deprecated [-Wdeprecated-declarations]
flaky f;
^
dep.cpp:3:1: note: 'flaky' declared here
flaky {
^

C++的屬性標記語法看上去有點不熟悉。在屬性列表中,[[deprecated]]被放在關鍵字(如class 或者enum)之後,實體名字之前。
這個標記有另外一種形式,它包含了一個資訊引數。由開發人員決定如何寫這個資訊。clang3.4忽略了這個資訊。

看下面的程式碼片段:

class
[[deprecated]] flaky {
};

[[deprecated("Consider using something other than cranky")]]
int cranky()
{
return 0;
}

int main()
{
flaky f;
return cranky();
}

5. 二進位制數字和數字分隔符

有兩個新功能不是驚天動地的,但他們確實代表了很好的句法結構的改善。這樣的很小的改變改善了程式碼可讀性,進一步減少了bug數量。C++ 程式設計師現在可以建立一個二進位制數字,向已經包含十進位制,十六進位制以及很少使用的八進位制的標準中又添加了一員。二進位制數字使用字首0b後面緊接數字。在美國和英國,我們使用逗號來作為數字分隔符,如:$1,000,000。這種寫法真正方便了讀者,使得我們的大腦處理很長的數字時更加容易。因為同樣的原因C++標準委員會添加了數字分隔符。它們不影響數值,只是通過分塊讓數字的讀寫更加容易。

數字分隔符使用什麼字元?在C++中基本上每個標點符號都被特定的特性使用了,因此沒有很明顯的選擇。最後的選擇是使用單引號字元,使得C++的百萬數表示如下:1'000'000.00。記住分隔符對數值沒有任何影響,因此百萬數也可表示如下:1'0'00'0'00.00。
下面的例子使用了兩個新特性:

#include <iostream>

int main()
{
int val = 0b11110000;
std::cout << "Output mask: "
<< 0b1000'0001'1000'0000
<< "\n";
std::cout << "Proposed salary: $"
<< 300'000.00
<< "\n";
return 0;
}
結果也是你所意料的:
Output mask: 33152
Proposed salary: $300000


6. 剩餘特性

c++的其他新特性無需多述。變數模板在變數上對模板的擴充套件。總會使用到的例子是變數pi<T>的一個實現。當實現為double的時候,變數會返回3.14,當實現為int時,它可能返回3,當實現為string時,可能返回“3.14”或者"pi",這是個很棒的特性,以前是在<limits>中實現的。變數模板的語法和語義和類模板是基本相同的——你無需額外的學習就能使用它們。對constexpr函式的限制放鬆了,例如,可以有多個返回值,可以在內部使用case和if語句,可以用迴圈以及其它。這就對能在編譯器做的事進行了擴充套件,為模板的引入插上了翅膀。其他小的特性包括為記憶體分配指定大小(sized deallocations)和一些語法的整理(tidying)。

相關部落格:

原文連結