使C++14 更加安全和更加方便的有用新特性

使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
return1.4;

對於程序員和編譯器來說,很顯然,函數返回的是double類型。因此在C++14中,程序員可以用auto代替double來定義函數返回類型:

1
auto getvalue() {

這個新特性需要注意的一個細節也是相當容易理解的。那就是,如果一個函數有多個返回路徑,那麼每個返回路徑返回的值需要具有相同的類型。

1
2
3
4
5
6
7
auto f(inti)
{
  if( i < 0 )
    return-1;
  else
    return2.0
}

上面這段代碼似乎顯然應該推導出返回類型是double,但是C++14禁止這種帶歧義性的使用。對於上述代碼,編譯器會報錯:

1
2
3
4
5
error_01.cpp:6:5: error: 'auto'in returntype deduced as 'double'here but deduced as 'int'in
      earlierreturnstatement
    return2.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>
 
structrecord {
   std::string name;
   intid;
};
 
auto find_id(conststd::vector<record> &people,
             conststd::string &name)
{
  auto match_name = [&name](constrecord& r) -> bool{
    returnr.name == name;
  };
  auto ii = find_if(people.begin(), people.end(), match_name );
  if(ii == people.end())
    return-1;
  else
    returnii->id;
}
 
intmain()
{
  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
structrecord {
   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<typenameContainer>
structfinder {
  staticdecltype(Container::find) finder1 = Container::find;
  staticdecltype(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>
 
intmain()
{
  std::vector<int> ivec = { 1, 2, 3, 4};
  std::vector<std::string> svec = { "red",
                                    "green",
                                    "blue"};
  auto adder  = [](auto op1, auto op2){ returnop1 + 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";
  return0;
}

上述代碼產生以下輸出:

1
2
intresult : 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){ returnop1+op2; } )
          <<"\n";

可初始化的Lambda捕獲

在C++11中,我們不得不開始適應lambda capture這一概念。其聲明指導編譯器進行closure的創建:closure是一個由lambda定義的函數的實例,同時,它綁定了定義於lambda作用域之外的變量。

在上文有關推導返回類型的示例中,定義了一個lambda,它捕獲變量name,該變量被作爲一個搜索字符串的謂詞函數的源:

1
2
3
4
auto match_name = [&name](constrecord& r) -> bool{
    returnr.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(newint);
  *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>
 
intmain()
{
  std::unique_ptr<int> p(newint);
  intx = 5;
  *p = 11;
  auto y = [p=std::move(p)]() { std::cout << "inside: " << *p << "\n";};
  y();
  std::cout << "outside: " << *p << "\n";
  return0;
}

在這個例子中,捕獲的變量值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")]]
intcranky()
{
   return0;
}
 
intmain()
{
  flaky f;
  returncranky();
}
1
2
3
4
5
6
dep.cpp:14:10: warning: 'cranky'is deprecated[-Wdeprecated-declarations]
  returncranky();
         ^
dep.cpp:6:5: note: 'cranky'declared here
intcranky()
    ^

二進制常量和單引號用作數字分位符

這兩個新特性並不驚天動地,但它們確實代表了好的語法改進。語言中像這樣的持續小改進可以提高代碼的可讀性並因此而減少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>
 
intmain()
{
  intval = 0b11110000;
  std::cout << "Output mask: "
            << 0b1000'0001'1000'0000
            <<"\n";
  std::cout << "Proposed salary: $"
            << 300'000.00
            <<"\n";
  return0;
}

這段代碼的輸出毫不令人吃驚:

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++應該能夠繼續作爲當性能被定爲目標時的首選語言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章