使C++更加安全和更加方便的有用新特性
今年8月,經過投票, C++14標準獲得一致通過。目前唯一剩下的工作是ISO進行C++標準的正式釋出。在本文中,我關注的是新標準中的幾個重要點,展示了即將到來的改變會如何影響你的程式設計方式,特別是在使用被現代C++稱之為習語和範型的特性時。
C++標準委員會決心使標準制定過程比過去10年更加快速。這意味著,距上一個標準(即C++11)僅3年的C++14是一次相對較小的釋出。這遠非一個令人失望的訊息,恰恰相反,這對程式設計師來說是個好訊息。因為這樣的話,開發人員能夠實時地跟上新特性。所以,今天你就可以開始使用C++14的新特性了—而且,如果你的工具鏈足夠靈活的話,你幾乎可以使用全部新特性了。
目前你可以從這裡得到標準草案的一份免費副本。遺憾的是,當最終版本的標準釋出時,ISO會進行收費。
縮短標準釋出的時間間隔可以幫助編譯器作者更實時地跟上語言變化。僅隔三年就再次釋出,需要調整以適應的變化也就更少。
本文的例子主要在clang 3.4上測試過,clang 3.4覆蓋了大多數C++14的新特性。目前,g++對新特性的覆蓋更少一些,而Visual C++似乎落後更多。
C++14:重大變化
接下來,本文將說明對程式設計師編碼工作會有重大影響的C++14特性,在給出例項的同時,還討論了何時何地因何使用這些特性。
返回型別推導
在這次釋出中,關鍵字auto的作用擴大了。C++語言本身仍然是型別安全的,但是型別安全的機制逐漸改由編譯器而不是程式設計師來實現。
在C++11中,程式設計師最初使用auto是用於宣告。這對於像迭代器的建立之類尤其有用,因為完整的正確的型別名可能長得可怕。使用了auto的C++程式碼則易讀得多:
1 |
for ( auto ii = collection.begin() ; ... |
在C++14中,auto的使用在好幾個方面得到了擴充套件。其中之一便是意義非凡的返回型別推導。在一個函式中編寫如下一行程式碼:這段程式碼依然完全地是型別安全的,因為編譯器知道begin()在上下文中應該返回什麼型別。因此,ii的型別是毫無疑問的,並且在使用ii的每個地方,編譯器都會進行檢查。
1 |
return 1.4; |
對於程式設計師和編譯器來說,很顯然,函式返回的是double型別。因此在C++14中,程式設計師可以用auto代替double來定義函式返回型別:
1 |
auto getvalue() { |
這個新特性需要注意的一個細節也是相當容易理解的。那就是,如果一個函式有多個返回路徑,那麼每個返回路徑返回的值需要具有相同的型別。
1 2 3 4 5 6 7 |
auto f(int i) { if ( i < 0 ) return -1; else return 2.0 } |
上面這段程式碼似乎顯然應該推匯出返回型別是double,但是C++14禁止這種帶歧義性的使用。對於上述程式碼,編譯器會報錯:
1 2 3 4 5 |
error_01.cpp:6:5: error: 'auto' in return type deduced as 'double' here but deduced as 'int' in earlier return statement return 2.0 ^ 1 error generated. |
為C++程式增加推導返回型別這一特性有諸多很好的理由。第一個理由是,有時候需要返回相當複雜的型別,例如,在STL容器中進行搜尋時可能需要返回迭代器型別。auto使函式更易編寫,更具可讀性。第二個(可能不那麼明顯的)理由是,auto的使用能夠增強你的重構能力。考慮以下程式:
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 |
#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"; } |
在這個例子中,使用auto代替int作為find_id()函式的返回型別並不能節省多少腦細胞J。但是,考慮一下,如果我決定重構record結構,將會發生什麼。或許我想用一個新的型別GUID而不是一個整型來標識record物件中的人:
1 2 3 4 |
struct record { std::string name; GUID id; }; |
record物件的變化將引起包括函式返回型別在內的一系列級聯變化。但是,如果我在函式中使用了自動的返回型別推導,那麼編譯器將默默地為我進行這些修改。
任何有過大型專案工作經驗的C++程式設計師都應該很熟悉這個問題–對單一資料結構的修改可能引起程式碼庫看似無窮無盡的迭代:修改變數,修改引數,修改返回型別。auto的增加使用對減少這種工作貢獻不小。
注意在上述例子及本文的餘下部分,我建立並使用有名的lambda。我猜想,大多數使用者在std::find_if()這樣的函式中都是把lambda定義為匿名的內聯物件的,這確實是非常方便的方式。由於瀏覽器的頁面寬度有限,我認為把lambda的定義和使用分開能夠使讀者通過瀏覽器閱讀程式碼比較容易。因此,這並不是各位讀者一定應該仿效的方式,讀者們只是應該感激這樣使程式碼更加易讀–特別是,當你是一位缺乏lambda使用經驗的讀者時。
說回auto,使用auto作為返回型別帶來的一個直接推論是其分身decltype(auto)的實現,以及它在型別推導時將遵循的規則。像下面的程式碼片段展示的一樣,現在你可以使用它自動地捕獲型別資訊:
1 2 3 4 5 |
template<typename Container> struct finder { static decltype(Container::find) finder1 = Container::find; static decltype(auto) finder2 = Container::find; }; |
泛型Lambdas
auto悄悄潛伏的另一個地方是lambda引數的定義。使用auto型別宣告來定義lambda引數等價於放鬆限制地建立模板函式。基於推匯出的引數型別,lambda將進行特定的例項化。
這方便了可重用於不同上下文的lambda的建立。在下文的簡單例子中,我建立了一個lambda,用來作為一個標準庫函式的謂詞函式。在C++11中,我需要明確地例項化一個lambda用於整數的相加,再例項化另一個lambda用於字串的相加。
有了泛型lambda後,我可以只定義一個帶有泛型引數的lambda。儘管泛型lambda在語法上沒有包含關鍵字template,但是很顯然,它仍是C++泛型程式設計的進一步延展。
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 |
#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; } |
上述程式碼產生以下輸出:
1 2 |
int result : 10 string result : redgreenblue |
即使你例項化匿名的內聯lambda,採用泛型引數仍然是有用的,原因我已在前文中討論過,這裡再複述一下–當你的資料結構改變時,或者API中獲取簽名的函式修改時,泛型lambda將在重新編譯時自行調整而不需要重寫程式碼。使用泛型引數的匿名內聯lambda例子如下所示:
1 2 3 4 5 6 |
std::cout << "string result : " << std::accumulate(svec.begin(), svec.end(), std::string(""), [](auto op1,auto op2){ return op1+op2; } ) << "\n"; |
可初始化的Lambda捕獲
在C++11中,我們不得不開始適應lambda capture這一概念。其宣告指導編譯器進行closure的建立:closure是一個由lambda定義的函式的例項,同時,它繫結了定義於lambda作用域之外的變數。
在上文有關推導返回型別的示例中,定義了一個lambda,它捕獲變數name,該變數被作為一個搜尋字串的謂詞函式的源:
1 2 3 4 |
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作用於來自其作用域之外的變數例項本身。
這一切都OK,但同時也帶來了一些限制。我認為,C++標準委員會覺得需要特別強調的一點是,不能使用move-only語法來初始化捕獲的變數。
這說明什麼呢?如果我們想把lambda作為一個引數的sink(接收器),我們會使用move語法來捕獲其作用域之外的變數。作為一個例子,考慮一下如何得到一個lambda,它接收具有move-only特點的unique_ptr物件。首先,嘗試通過值捕獲將以失敗告終:
1 2 3 |
std::unique_ptr<int> p(new int); *p = 11; auto y = [p]() { std::cout << "inside: " << *p << "\n";}; |
這段程式碼產生編譯錯誤是因為unique_ptr不會生成拷貝建構函式–unique_ptr本身就是為禁止拷貝而生的。
修改程式碼通過引用捕獲p能夠編譯通過,但是這並不能達到期望的效果,我們的初衷是通過移動變數的值到本地拷貝來接收變數值。最終,建立一個區域性變數並在通過引用捕獲時呼叫std::move()能夠達到目的,但是其效率略低。
修改捕獲子句的語法可以解決效率低的問題。現在,不僅僅可以宣告一個捕獲變數,還可以進行初始化。作為標準中的一個例子,簡單情形下的使用看起來像這樣:
1 |
auto y = [&r = x, x = x+1]()->int {...} |
它捕獲x的副本同時實現對x的增量操作。這個例子很容易理解,但是我不確定它是否能夠捕獲這種新語法下的move-only變數的值。一個利用了這個新語法的用例如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#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語法進行初始化,在不需要宣告一個區域性變數的情況下有效地接收了指標。
1 2 |
inside: 11 Segmentation fault (core dumped) |
這個惱人的結果正是你所期望的–程式碼在變數p已經被捕獲並移動到lambda後試圖解引用它(這當然會導致錯誤)。
[[deprecated]]屬性
當我第一次在Java中見到deprecated屬性的使用時,我承認我有點嫉妒這門語言。對大多數程式設計師來說,程式碼陳舊是個大問題。(有因刪除程式碼而被稱讚過嗎?反正我從來沒有。)這個新屬性提供瞭解決這個問題的系統方法。
它的用法方便又簡單—只需要把[[deprecated]]標籤放到宣告的前面即可—可以是類,變數,函式,或者其他一些實體的宣告。結果看起來像這樣:
1 2 3 |
class [[deprecated]] flaky { }; |
當程式中使用了過時的實體時,編譯器的反應是把它留給開發人員。顯然,大多數人會希望在需要時看到某種形式的警告,同時在不需要時也能夠關掉警告。clang3.4中有一個例子,當例項化一個過時的類時給出了警告:
1 2 3 4 5 6 |
dep.cpp:14:3: warning: 'flaky' is deprecated [-Wdeprecated-declarations] flaky f; ^ dep.cpp:3:1: note: 'flaky' declared here flaky { ^ |
你可能已經注意到,C++的attribute-tokens語法看起來似乎有點不常見。包含[[deprecated]]的屬性列表,被放在class,enum等關鍵字之後,實體名之前。
這個標籤具有包括訊息引數的另一種形式。同樣地,如何處理該訊息取決於開發人員。顯然,clang3.4直接忽略了該訊息。因為,如下程式碼片段的輸出中並不包含錯誤訊息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class [[deprecated]] flaky { }; [[deprecated("Consider using something other than cranky")]] int cranky() { return 0; } int main() { flaky f; return cranky(); } |
1 2 3 4 5 6 |
dep.cpp:14:10: warning: 'cranky' is deprecated [-Wdeprecated-declarations] return cranky(); ^ dep.cpp:6:5: note: 'cranky' declared here int cranky() ^ |
二進位制常量和單引號用作數字分位符
這兩個新特性並不驚天動地,但它們確實代表了好的語法改進。語言中像這樣的持續小改進可以提高程式碼的可讀性並因此而減少bug數量。
除了原有的十進位制、十六進位制和比較不常用的八進位制表示方法之外,C++程式設計師現在還可以使用二進位制表示常量了。二進位制常量以字首0b(或0B)開頭,二進位制數字緊隨其後。
在英美兩國,在寫數字時,我們習慣於使用逗號作為數字的分隔符,如:$1,000,000。這些數字分隔符純為方便讀者,它提供的語法線索使我們的大腦在處理長串的數字時更加容易。
基於完全相同的原因,C++標準委員會為C++語言增加了數字分隔符。數字分隔符不會影響數字的值,它們的存在僅僅是為了通過分組使數字的讀寫更容易。
使用哪個字元來表示數字分隔符呢?在C++中,幾乎每個標點字元都已經有特定的用途了,因此並沒有明顯的選擇。最終的結果是使用單引號字元,這使得百萬美元在C++中寫作1’000’000.00。記住,分隔符不會對常量的值有任何影響,因此,1’0’00’0’00.00也是表示百萬。
下面是一個結合了這兩種新特性的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#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; } |
這段程式碼的輸出毫不令人吃驚:
1 2 |
Output mask: 33152 Proposed salary: $300000 |
其他
C++14規範中的其他特性並不需要如此多的闡釋。
變數模板就是將模板擴充套件到變數。用濫了的例子是變數模板pi<T>的實現。當T表示double型別時,變數返回3.14。表示int型別時,返回3。表示std::string型別時,則可能返回字串”3.14”或者”pi”。當<limits>標頭檔案寫好的時候,這將是一個很好的特性。
變數模板的語法及語義與類别範本幾乎是相同的,所以,即使不進行任何額外的學習,使用它們也應該是沒有問題的(如果你已經瞭解了類别範本的話)。
constexpr函式的限制被放鬆了。現在允許在case語句,if語句,迴圈語句等語句中進行多處返回了。這擴充套件了可在編譯期間完成的事情的範圍,增加可在編譯期間完成的事情這一趨勢在模板被引入後發展得尤其迅速。
其他的小特性包括可指定大小的資源回收函式和一些語法整理。
接下來
C++標準委員會明顯感受到了壓力,正在通過改進來保持C++語言與時俱進。在這個十年期中,他們已經在至少一個(即C++17)以上的標準上進行努力了。
也許更有趣的是,幾個衍生組織的創立,這些組織可以建立技術規範文件。這些文件不會提升為標準,但是它們會發表並獲得ISO標準委員會的認可。根據推測,這些事務將以更快的速度得到推進。這些組織當前工作的八大領域包括以下方面:
- 檔案系統
- 併發性
- 並行性
- 網路
- C++的AI概念(Artificial Intelligence,人工智慧)–一直處於規範中。
這些技術規範的成功與否取決於其是否被採納和使用。如果我們發現所有開發人員都跟隨它們,那麼這種進行標準化的新途徑就算成功了。
這些年來C/C++發展良好。現代C++(或許以C++11作為開始)在保持效能的同時,在使C++語言更加易用更加安全方面取得了引人注目的進展。對於某些型別的工作,你很難找出C/C++之外的任何合理替代品。C++14並未做出C++11版本中那樣的大改進,但是它把語言保持在一條很好的路上。如果C++標準委員會在未來十年保持其目前的效率,那麼C++應該能夠繼續作為當效能被定為目標時的首選語言。
原文作者Mark Nelson是Dr. Dobb的頻繁貢獻者,同時也是《Data Compression Book》一書的主要作者。