C++的新特性

峻峰飛陽發表於2015-04-30

介紹

      也許你已經意識到了,在ISO 標準中C++語言已經被更新了。對於新的C++ 語言的編碼名字已經改為C++0x, 許多編譯器都已經介紹了它的一些特性。這個指南將嘗試給你介紹C++ 語言的新特性。請注意,儘管這些特性已經應用到其他的編譯器上,但我只在 Visual C++ 2010 編譯器上解釋一些新的特性。在其他編譯器上絕對的語法規則我可不敢解說。

      這篇文章假設你有一定的C++ 語言基礎,並且瞭解型別轉換,常量方法,並且知道模板庫(基本的意識).

C++的新特性

      以下列表是我將要討論的C++ 語言增加的一些新特性。我已經更多的強調了lambdas 和R-values (這兩個新名詞暫時沒有合適的漢語翻譯)了,由於我還沒有在哪裡找到任何可消化的東西,所以現在,我將不會使用模板或標準模板庫來簡單舉例,但是我將一定會增加和更新這部分內容的。

關鍵字:autokeyword

自動資料型別演變(在編譯時)依靠分配

關鍵字:decltype

推斷資料型別從表示式或 auto 變數而來

關鍵字:nullptr

空指標已經被提倡,並且演變成一個關鍵字!

關鍵字:static_assert

對於編譯時間的斷言。對模板和有效性不能使用巨集#ifdef 是很有用的 

Lambda 表示式

邏輯定義的函式。從函式指標和類物件中繼承

Trailing 返回型別

但模板的返回型別不能夠表示式,是很有用的

R-value 引用

在模板物件被銷燬之前移除語義資源的利用。

其他語言特性

C++ 語言的特性一定被包含在VC8 和VC9 (VS2005, VS2008) 中,在並沒有增加進C++ 標準中. 這些特性現在已經被整理到 C++0x中.

這篇文章將詳細(但不是全部)介紹這些特性。

讓我們開始吧!


關鍵字 'auto' 

auto 關鍵字,現在有更多的含義。我假設你瞭解這些關鍵字的原始意義。使用這個修訂的關鍵字你可以在不需要制定其資料型別的情況下宣告一個變數。

例如:

auto nVariable = 16;

以上程式碼宣告瞭nVariable變數,並沒有指定其型別。使用右邊的表示式,編譯器會判斷出變數的型別。像如上的程式碼會被編譯器翻譯成如下所示 :

int nVariable = 16;

你可能會斷言,對變數的分配現在是強制的。因此,你不能像如下宣告一個自動變數:

auto nVariable ;  

// Compilererror:

// errorC3531: 'nVariable': a symbol whose type contains

//              'auto' must have an initializer

現在,編譯器就不知道變數nResult 的型別,使用auto關鍵字:

  • 變數的型別在編譯時,而不是在執行時被確定.

已經說過,無論你得任務有多複雜,編譯器仍夠能確定資料型別。如果編譯器不能夠判斷出型別。它將產生一個錯誤。這不像是Visual Basic或web指令碼語言。

幾個例子

auto nVariable1 = 56 + 54; // int 型別的推論

auto nVariable2 = 100 / 3;  // int 型別的推論

 

auto nVariable3 = 100 / 3.0; // double 型別的推論. 由於編譯器把整個表示式作為double

auto nVariable4 = labs(-127); // long, 由於 'labs'函式的返回值為long 型別

讓我們來點稍微複雜一點的 (繼續下面的宣告):

// nVariable3被推論為 double.

auto nVariable5 =sqrt(nVariable3);

// 被推論的 double, 因為 sqrt 接受 3 不同的資料型別,但是我們傳入double,覆蓋返回值為

// double!

 

auto nVariable =sqrt(nVariable4); 

// 錯誤,因為 sqrt 的呼叫時模稜兩可的.

// 這個錯誤與 'auto' 關鍵字不相關!

指標的推論:

auto pVariable6 =&nVariable1; // 被推論為'int*', 由於nVariable1 是 int型別.

auto pVariable =&pVariable6; // int**

auto* pVariable7 =&nVariable1; // int*

引用的推論:

auto & pVariable =nVariable1;  //int &

// 是的,又該這個變數的值,將修改其引用的變數! 

使用 new 操作符:

auto iArray = new int[10]; // int* 

使用 const 和 volatile 修飾符:

const auto PI = 3.14;  // double

volatile auto IsFinished = false;  // bool

const auto nStringLen = strlen("CodeProject.com");

不允許的情景

陣列不能被宣告自動型別:

 

auto aArray1[10];

auto aArray2[]={1,2,3,4,5};

// errorC3532: 陣列型別的的元素的型別不能定義為包含 'auto'

不能為函式的引數或返回型別:

auto ReturnDeduction(); 

 

void ArgumentDeduction(auto x);

// C3533:'auto': 引數型別的變數不能夠被宣告包含 'auto'

如果你需要一個 auto的返回型別或auto 的引數,你可要簡單的使用模板!

你不能在 類class或結構體struct中使用auto,除非它是一個靜態的成員;

struct AutoStruct 

{

    auto Variable = 10;

    // errorC2864: 'AutoStruct::Variable' : 只有靜態只讀的成員變數才可在結構體或類中  被初始化

};

你不能在一個自動變數中包含多種資料型別(型別被推論為不同的型別):

auto a=10, b=10.30, s="new";

// errorC3538: 在一個 declarator-list 'auto'

//             必須被推論成相同的資料型別

像如上,如果你使用不同的函式初始化變數,一個或多個函式返回不同型別的資料型別,編譯器一定會產生相同的錯誤 (C3538如下):

auto nVariable = sqrt(100.0), nVariableX =labs(100); 

你可以在全域性的層次上使用關鍵字auto.

這個變數似乎是你得老朋友,什麼都懂,但是如果你濫用的話講是一種詛咒。例如,少部分程式設計師假想如下的方式宣告float ,但實際上為double。

auto nVariable = 10.5; 

相似,如果一個函式的返回值為int 。接下來你修改其返回值的型別為short 或double,原來的自動變數將變得小心翼翼。 如果你不幸,只有一個變數使用了auto宣告,編譯器將推斷這個變數為新的資料型別。並且,如果你很幸運, 與其他型別變數混合使用,編譯器將會產生C3538的錯誤(如上)。

因此,我們什麼時候應該真正的使用 'auto' 關鍵字呢?

1. 但一個資料型別依靠編譯器和/或目標平臺而有可能改變時

For example:

int nLength = strlen("The auto keyword.");

可能在一個32 位的編譯環境下返回一個4位元組的整型,但是在一個64位的環境下可能返回8位。很確定的事情,你可以使用 size_t代替 int或 __int64。然而沒有定義size_t的程式碼編譯會怎樣呢!或者 strlen返回別的什麼?在這種情況下,你能夠簡單的使用auto:

auto nLength = strlen("The auto keyword.");

2. 當資料型別的表示比較複雜的時候,或它使得程式碼很混亂

std::vector<std::string>Strings; // string vector

 // Push severalstrings

 

// 如此顯示出它們

for(std::vector<std::string>::iterator iter = 

    Strings.begin(); iter !=Strings.end();  ++iter)

{  std::cout <<*iter << std::endl; }

你知道,打出這樣的型別std::vector<std::string>::iteratoriter通常是很笨拙的。儘管可以選擇使用關鍵字typedef 在程式的某些地方來返回型別並且使用型別名。但是對於迭代器型別又有多少呢? 並且一個迭代器型別只用一次又會怎樣呢? 我們像如下的方式縮短了編碼:

 

for(auto iter = Strings.begin(); iter != Strings.end(); ++iter)

{ std::cout << *iter <<std::endl; }

如果你已經有一段時間使用了STL 了,你會了解迭代器和只讀迭代器的。如上的例子,你也是會傾向於使用const_iterator來代替iterator。 因此,你也許想這樣使用:

// Assume'using namespace std'

for(vector<string>::const_iteratoriter = 

          Strings.begin(); iter !=Strings.end(); ++iter)

{ std::cout << *iter <<std::endl; }

因此,使迭代器成為只讀,以便於vector的元素不能夠被修改。請記住,當你在一個const 的物件上呼叫 begin方法時, 你不能夠再將它分到一個非只讀的iterator 上, 並且你必須把它分配到const_iterator上(const iterator和const_iterator不一樣, 因為我不是在寫STL, 請自己閱讀並做相關實驗)。

概括了所有的複雜性,並且引進了auto關鍵字,標準的C++為STL 容器為使用cbegincendcrbegincrend方法提供了便利。C 字首意思為只讀。他們總是返回const_iterator的迭代器,不管物件(容器)是否為const。老的方法依靠物件的常量性返回迭代器的兩種型別。

// 在cbegin使用的情況下,迭代器總是隻讀的.

for(auto iter = Strings.cbegin(); iter!=Strings.cend(); ++iter) {...}

另外一個例子在一個迭代器/只讀迭代器上:

map<std::vector<int>,string> mstr;

 

map<vector<int>,string>::const_iterator m_iter =mstr.cbegin();

迭代器的宣告可以被縮短為如下:

auto m_iter =mstr.cbegin();

就像複雜的模板一樣,你可以使用關鍵字來分配一個難以確定型別,易於出錯的函式指標,這些也許可從其他的變數/函式上分配。由於沒有例子給出,我假定你能夠明白我的意思。 

3. 為變數分配一個 Lambdas(匿名程式)

以下文章是關於lambdas的解釋。

4. 為返回型別指定一個Trailing

儘管不涉及到匿名程式,學習(lambda)匿名錶達式語法是必須的。我將在匿名程式後面討論它。

外部引用


'decltype' 關鍵字

C++ 操作符給出表示式的型別。 例如:

int nVariable1;

...

decltype(nVariable1)  nVariable2;

宣告nVariable2為一個int型別的。編譯器知道nVariable1的型別,並且將decltype(nVariable1)翻譯成int型別。decltype關鍵字與typeid關鍵字不同。typeid操作符返回type_info的結構體,並且要求RTTI 是使能的。由於它返回的是型別資訊,而不是型別本身,你不能像如下的方式使用typeid:

typeid(nVariable1) nVariable2;

然而,推斷出表示式為一個型別,這完全是在編譯時刻。你不能夠使用decltype獲得型別名(如'int')。

decltype與auto結合使用是典型的應用。例如,你宣告自動變數如下:

auto xVariable =SomeFunction();

假設xVariable(實際上,為SomeFunction的返回型別)的型別是X.現在,你不能再次呼叫(或者不想呼叫)這個函式了。你怎樣來宣告另外一個相同型別的變數呢?

下面那一個會更適合你呢?

decltype(SomeFunc) yVar;

decltype(SomeFunc())yVar;

第一個宣告yVar為函式指標,第二個為型別X。使用函式名字不是很可靠的,你照樣可以訪問,編譯器不會給出錯誤提示或警告直到你使用變數。通常你必須傳遞實際的引數值,且函式/方法的引數的實際型別是被覆蓋的。

推薦的方式是直接通過型別來推論:

decltype(xVariable) yVar;

況且,因為你已經看到關於auto的討論了,使用模板型別是複雜和不美觀的,你應該使用auto。同理,你應該使用decltype宣告一個正確的型別:

decltype(Strings.begin())string_iterator;

decltype(mstr.begin()->second.get_allocator()) under_alloc;

不像是先前的例子那樣,然而我們使用auto從右邊的表示式上推論出型別, 我們不需要指定的去推論型別。使用decltype,你不需要指定變數,僅僅宣告它——因為型別是預先知道的。當你使用表示式Strings.begin()時,函式並沒有被呼叫,它僅僅是從表示式上推論出型別來。相似的,當你把表示式放到decltype中時,表示式並沒有被求值。只有基本的語法檢查被執行。

在上面的第二個例子中,mstr是std::map物件,我們取回迭代器,即為map元素的第二個成員,並最終為它的分配器型別。因此,為string推斷出std::allocator型別(請看上面,mstr被宣告)。

幾乎沒好處,但有點悖理,例子如下:

decltype(1/0) Infinite; // 對於除數為0的情況,編譯器並不產生錯誤

//程式不會 'exit', 會儲存 exit(0), 的型別.

//在不進行函式呼叫的情況下指定exit 會使得'MyExitFunction' 的返回型別有所不同

decltype(exit(0)) MyExitFunction(); 

外部參考

decltype 的型別來自 - MSDN


關鍵字 'nullptr'

空指標最終以關鍵字的形式分類!它和NULL巨集和整形0很相似。儘管我只談論本地C++, 但是要知道,關鍵字不但用於本地C++, 而且應用於託管的程式碼中。如果你以混合的模式使用C++, 你可以顯式的的使用關鍵字__nullptr來陳述本地空指標,並且使用nullptr表示託管空指標。即使在混合的模式下程式設計,你不會常使用__nullptr。

void* pBuffer = nullptr;

...

if ( pBuffer == nullptr )

// Do something with null-pointer case }

 

 

// Withclasses

 

void SomeClass::SomeFunction() 

{

   if ( this != nullptr)

   { ... }

}

請記住,nullptr是一個關鍵字,不是型別。因此,對於它你不能使用操作符sizeof或decltype。已經說過,NULL巨集和關鍵字nullptr是不同的兩個實體。NULL 就是0,其實就是一個int 型別的。  

例如:

void fx(int*){}

 

void fx(int){}

 

int main()

{

    fx(nullptr);// 呼叫  fx(int*)

    fx(NULL);   // 呼叫  fx(int)

 

}

Externalreferences

(使用 /clr 選項編譯器要求是錯誤的. MSDN 沒有更新, 寫這篇文章之時.)


'static_assert' 關鍵字

使用static_assert關鍵字,你能夠在編譯時指定一些條件。這裡是語法規則:

static_assert(expression, message)

表示式要成為一個編譯時的常量表示式。對於非模板的靜態宣告,編譯器立刻會認定出表示式來。對於模板的宣告,編譯器測試斷言,什麼時候類被例項化。

如果表示式為真,就意味著你要求的斷言已經滿足,並且陳述不會做任何事情。如果表示式為假,編譯器會產生一個C2338 的錯誤提示。例如:

static_assert (10==9 , "Nine is not equal to ten"); 

很顯然的,不為真,所以編譯器會產生如下錯誤:

error C2338: Nine is notequal to ten

更有意義的斷言會產生,當程式沒有在32位編譯器下編譯時:

static_assert(sizeof(void *) == 4, 

       "Thiscode should only be compiled as 32-bit.");

由於任何一個指標型別的大小是相同的,當選擇目標平臺做編譯時。

在早期的編譯器中,我們需要使用_STATIC_ASSERT,儘管它不會做任何事情,但是宣告瞭陣列的大小狀況。因此,如果條件為真,它將宣告大小為1的陣列;如果條件為假, 它將宣告大小為0 的陣列——這將導致編譯器產生一個錯誤。錯誤通常不是友好的。

  • error C2466:不能夠分配固定大小為0 的陣列

外部引用


匿名(Lambda)表示式

這是C++ 增加的一個引人注目的語言特性。非常有用的,有意思的,並且也是複雜的!我將以最基本的語法和例子展開闡述, 使得它變得更加清晰。因此以下幾行程式碼也許不是lambdas的使用特性。但是,可以確定,lambdas 是很有效的,而且也是C++玉雅簡潔性的特性!

在開始討論之前,讓我首先概括一下:

  • lambdas 更像一個本地定義的函式。你可以在程式的任何地方去實現一個lambdas,可以像代替正常的表示式或像正常的函式一樣被呼叫(想起來了在之前的VC++ 編譯器中會報錯“本地函式的定義是非法的?”)。

最最基本的lambda:

 []{};  // 在某些函式中/或程式碼塊中 而不是全域性的.

是的,上面的宣告是完全合法的 (只在 C++0x!).

[]是匿名錶達式的引入符號,這個符號告訴編譯器下面的表示式是一個匿名的。{}是匿名錶達式的定義部分,就像任何函式/方法一樣。以上的匿名錶達式沒有傳遞任何引數,不會返回任何值,當然也不會做任何事情。

讓我們繼續...

 []{ return 3.14159; }; // 返回一個 double

以上的匿名程式碼可以簡單的工作:返回PI的值。但是誰會呼叫這個lambda呢?返回值又會去哪裡呢? 讓我們進一步來討論:

double pi = []{ return 3.14159; }(); // Returnsdouble

你意識到了嗎? 返回值被儲存到一個區域性變數pi中. 通常, 注意上面的例子,匿名函式被呼叫(注意最終的函式呼叫). 下面是最少的執行程式程式碼:

int main()

{

   double pi;

   pi = []{return3.14159;}(); 

   std::cout <<pi;

}

匿名錶達式的最後一個圓括號是對匿名函式的一個呼叫。 這裡,匿名錶達式沒有攜帶任何引數,但是操作符()在其結尾仍然需要,以便於讓編譯器知道這是一個呼叫。pi的匿名錶達式也許可以像如下的方式被實現:

pi = [](){return 3.14159;}(); // 注意,第一個圓括號.

這個表示式更像:

pi = [](void){return 3.14159;}(); // 請注意 'void'

儘管,你是否在第一個圓括號中放入引數是一個選擇。但是我更加傾向於你放入引數。 C++標準委員會想使得匿名錶達式看起來稍微簡單一點,這就(也許就)是他們把匿名參量作為可選參量的原因吧。

讓我們更近一步,帶著匿名引數來討論:

bool is_even;

is_even = [](int n) { returnn%2==0;}(41); 

第一個圓括號,(int n),指定了匿名錶達式的參量。第二個圓括號,(41),給匿名錶達式傳進一個值。匿名錶達式的主體部分的功能為判斷傳入的數字是否能被2整除。現在我們可以實現一個判斷最大,或最小值的匿名錶達式,如下所示:

int nMax = [](int n1, int n2) {

return (n1>n2) ? (n1) : (n2);

} (56, 11);

 

int nMin = [](int n1, int n2) {

return (n1<n2) ? (n1) : (n2);

} (984, 658);

這裡,不去獨立的宣告和分配變數,我把他們放到同意行。匿名錶達式現在接收兩個引數,返回其中一個,這個值被分配給nMin 或nMax. 同理,匿名錶達式可以傳進更多的引數,並且也可接收更多引數型別。

你應該會有幾個問題:

  • 返回值是什麼呢? 只有 int 是有效的嗎? 
  • 匿名錶達式不止一個返回狀態又會怎樣呢? 
  • 匿名錶達式需要做別的事情,如顯示一個值,執行別的程式步驟又會怎樣呢? 
  • 我能否把一個引用儲存到定義的匿名錶達式中,並且在別的地方再次引用呢? 
  • 一個匿名錶達式能否呼叫另一個匿名錶達式或函式呢? 
  • 一個匿名錶達式被定義成一個區域性函式,它能否穿越函式的作用域呢? 
  • 一個匿名錶達式可以在一個變數定義或呼叫的地方訪問這個變數呢?能修改該這個變數的值嗎? 
  • 它支援預設引數嗎? 
  • 它與函式指標或函式物件(函子)有怎樣的不同呢 ? 

在我一個個回答如上問題時,請讓我使用如下的列表來向你展現匿名錶達式的語法規則:

Q. 關於返回值是什呢?

你可以在操作符 -> 後面指定返回值型別。例如:

pi = []()->double{ return 3.14159; }(); 

請記住,如上表所示,如果匿名函式僅包含了一條陳述(即,只有一個返回語句),就不需要指定返回型別。因此,在上面的例子中,指定返回值為double是可選的。對於自動可推論的返回型別你是否顯式的指定其返回型別完全由你決定。

一個必須強制指定返回型別的例子:

int nAbs = [] (int n1) -> int 

{

    if(n1<0)

       return-n1;

    else

       returnn1;

}(-109);

如果你沒有指定 -> int, 則編譯器會產生:

error C3499: 一個匿名錶達式已經被指定成一個空的返回型別不能夠再返回一個值

如果編譯器一個return宣告都沒有發覺:它就推論匿名函式有一個空的返回型別。這個返回型別可以是任何型別。 

 []()->int*{ }

[]()->std::vector<int>::const_iterator&{}

[](int x) ->decltype(x) { }; // Deducing type of 'x'

它不能返回一個陣列。也不能有一個自動變數型別的返回值:

 []()-> float[] {};  // error C2090: 函式返回了陣列

[]()-> auto {};    //error C3558: 'auto': 匿名函式的返回值不能包含 'auto'

當然,你可以使用一個自動變數來接收匿名函式的返回值:

auto pi = []{return 3.14159;}();

 

auto nSum = [](int n1, int n2, int n3)

{ return n1+n2+n3; } (10,20,70);

 

auto xVal = [](float x)->float 

    float t; 

    t=x*x/2.0f; 

return t;

} (44);

最後一點,如果你為有參量的匿名函式指定一個返回型別,你必須使用圓括號,下面的程式碼將產生錯誤:

 []->double{return 3.14159;}();  // []()->double{...}

Q. 匿名函式不止一個返回表示式會是什麼情況呢? 

上述的解釋已經足夠的說明了匿名函式能夠包含任何常規函式所包含的程式碼。匿名函式能夠包含任何函式\方法所包含的——區域性變數,靜態變數,呼叫其它函式,記憶體分配,和其他匿名函式!如下的的程式碼是有效的(儘管有點荒誕!):

[]()

{

   static int stat=99;

   classTestClass 

   { 

      public:

        intmember;

   };

 

   TestClass test; 

   test.member= labs(-100);

 

 

   int ptr =[](int n1) ->int*

   {

      int* p = new int;

      *p = n1;

      return p;

   }(test.member);

 

   delete ptr;

};

Q. 匿名函需要做一些事情,如顯示一個值,執行別的一些指令將會是怎樣的呢?我能否把一個引用儲存到一個已經定義的匿名函式中,並在一些地方再次使用? 一個匿名函式被定義成一個區域性函式,能否穿越函式作用域來使用呢?

讓我們定義一個匿名函式來判斷一個數值是否為偶數。使用auto關鍵字,我們能夠把匿名函式儲存在變數中。因此我們可以使用這個變數(也就是說,一個對匿名函式的呼叫)!我接下來會討論匿名函式的型別。像如下的簡單的定義:

auto IsEven = [](int n) -> bool 

     if(n%2 == 0)

        return true;

     else

        return false; 

};  // 沒有函式呼叫

就像你猜測的那樣,匿名函式的返回值為一個 bool型別的,它接收一個引數。並且很重要的是,我們沒有呼叫匿名函式, 只是定義了它。如果我們使用()並攜帶引數 ,變數的型別應該為bool型別,並不是匿名函式型別!如上宣告後,現在區域性定義的匿名函式可以被呼叫。

IsEven(20);

 

if( ! IsEven(45) )

      std::cout <<"45 is not even"; 

對IsEven的定義,如上已經給出,在同一個函式中會有兩次呼叫。如果你想在別的函式中來呼叫匿名函式會是怎樣的情況呢? 而這裡有一種應用,如儲存進一些區域性或類級別的變數中,然後把它傳進另外一個函式(就像一個函式指標),並且從別的函式中呼叫。另一種機制是以全域性的範圍來儲存和定義一個函式。由於我們沒有討論匿名型別的含義,我們將稍後討論第一種應用,下來讓我們討論第二種應用吧(全域性範圍):

Example:

// 匿名函式的返回值為 bool 

// 匿名函式被儲存到IsEven, 使用自動型別變數

auto IsEven = [](int n) -> bool 

   if(n%2 == 0) return true; 

   else return false; 

}

 

void AnotherFunction()

{

   // 呼叫它!

   IsEven (10);

 

int main()

{

    AnotherFunction();

    IsEven(10);

}

由於auto是可用於區域性和全域性範圍的,我們能夠使用它儲存匿名錶達式。我們需要知道型別以便於能夠把它儲存到類變數中,接下來使用。

早先已經說過,匿名函式幾乎能夠做常規函式所做的一切,因此顯示一個值對匿名函式來說並不是遙不可及的事情。

int main()

{

    using namespace std;

 

    auto DisplayIfEven= [](int n) -> void 

    {

        if (n%2 == 0)

            std::cout <<"Number is even\n";

        else

            std::cout <<"Number is odd\n";

    }

    

    cout <<"Calling lambda...";

    DisplayIfEven(40);

}

一個重要的結論要注意——一個區域性定義的匿名函式不能夠從它被定義進的上一級作用域中獲得名稱空間的解決方案。因此std名稱空間的範圍對於DisplayIfEven是不可用的。

Q. 一個匿名函式能否包含另外一個匿名函式或常規函式嗎? 

已經很明確,你已經知道了提供一個匿名錶達式/函式的名字是在呼叫的時刻,就像函式的呼叫要在函式中一樣的要求。

Q. 匿名函式支援預設引數嗎? 

不。

Q.一個匿名函式表示式可以從它定義或呼叫的地方訪問變數嗎?能否修改變數內容?它與函式指標,或函式物件(函子)有什麼不同嗎?

現在我將討論我遺留的問題:捕獲說明。

匿名錶達式可以是如下的情況:

  • 狀態的
  • 無狀態的

狀態定義了變數在一個更高一級的作用域中是怎樣被捕獲的。我從下面的類別中明確它:

  1. 沒有變數在它的上一級作用域中被訪問。這條我們一直在使用. 
  2. 變數在只讀模式下被訪問。你不能修改上一級變數的值。 
  3. 變數被拷貝到匿名函式中(以相同的名字),你可以修改拷貝的變數,這類似於函式的傳值呼叫機制。 
  4. 對於上一級的變數作用域,你擁有完全的訪問許可權,你可以使用相同的名字修改變數。 

你會明白以下4條是繼承至C++ 的優點:

  1. private變數, 你無法訪問它。 
  2. const (常量)方法中, 你不能修改變數。 
  3. 對於函式/方法的變數是通過傳值的。 
  4. 變數在方法中是完全訪問的。或者說,變數是通過引用傳遞的。

讓我們來介紹一下捕獲!捕獲的說明在上面的圖中已經提到,是通過[]給出的。下面的語法用於指定捕獲規格說明:

  •  [] – 什麼都不會捕獲。 
  • [=] – 通過值來捕獲。 
  • [&] – 通過引用來捕獲。 
  • [var] – 通過值來捕獲 var。 
  • [&var] – 通過引用來捕獲var。

例 1:

int a=10, b=20, c=30;

 

[a](void)  // 僅通過值來捕獲 'a' 

{

    std::cout <<  "Value ofa="<< 

    a <<std::endl;

        

    // 不能夠修改

    a++; // error C3491: 'a': a 是通過值來捕獲的 

          //       在一個非異變的匿名錶達式中不能夠被修改

 

    // 不能訪問其他變數

    std::cout <<b << c;

    // errorC3493: 'b'不能夠被隱式的捕獲

// 因為沒有預設的捕獲模式被指定 

}();

例 2:

auto Average = [=]() -> float  // '=' 意思是: 通過值來捕獲所有變數

{

    return ( a+ b + c ) / 3.0f;

 

   // 不能修改任何變數的值

};

 

float x = Average();

例 3:

// 使用 '&' 你指定了所有的變數通過引用來捕獲

auto ResetAll =[&]()->void 

{

    // 由於你是通過引用來捕獲變數的,你就不能夠修改他們!

    a = b = c = 0;

};

 

ResetAll();

// a,b,c 的值被設為0;

放置一個 = 指定通過值來捕獲。放置一個 & 指定通過引用來捕獲。 現在讓我們進行更多的探索, 言簡意賅, 我並不是把匿名錶達式放進一個自動變數中再去呼叫它。相反,我直接引用。

例 4:

// 通過值僅捕獲 'a' 和 'b' 

int nSum = [a,b] // 你還記得 () 對於無引數的匿名函式是可選的嗎?

    return a+b;

}();

 

std::cout << "Sum: "<< nSum;

如例4 所示,我們使用匿名錶達式的引入符號([]操作符)可以進行多個引數的捕獲。我來舉出第二個例子, 而所有的三個引數(a, b, c)的和被儲存在nSum中。

例 5:

// 只有'nSum'是通過引用捕獲,其他均通過值捕獲

[=, &nSum]

{

    nSum = a+b+c;

}();

在上例中,通過值來捕獲所有變數(即=操作符)指定了預設的捕獲模式,表示式&nSum 覆蓋了它。注意,預設的捕獲模式對所有的捕獲必須出現在其他捕獲之前。因此=&必須出現在其他指定之前,如下宣告將引發錯誤:

// & 或 = 必須首先出現(如果被指定).

[&nSum,=]{}

[a,b,c,&]{} // 邏輯上與上述相似,但是出錯的.

幾個例子:

 [&, b]{}; //(1) 通過引用來捕獲,除過‘b’為值捕獲之外

 [=, &b]{}; //(2) 與上述相反

[b,c, &nSum]; // (3) 通過值來捕獲‘b’,‘c’

                  //'nSum' 為值捕獲. 

[=](int a){} // (4) 所有通過值捕獲,隱藏了 'a' – 因為 a 現在是一個函式的引數,糟糕的練習,//編譯器不會產生警告            

[&, a,c,nSum]{}; // 與 (2) 相似

[b, &a, &c,&nSum]{} // 與 (1)相似

[=, &]{} // 無效!

[&nSum, =]{} // 無效!

[a,b,c, &]{} // 無效!

就像你看到的那樣,對於相同集的變數的捕獲是多種結合方式的。我們可以通過增加如下的方式來擴充套件指定的捕獲:

  • [&,var] – 除過var是通過值捕獲的,其他均通過引用捕獲。 
  • [=, &var] -除過var是通過引用捕獲的,其他均通過值來捕獲。 
  • [var1, var2] – 通過值來捕獲變數var1, var2  。 
  • [&var1, &var2] – 通過引用來 var1, var2 。 
  • [var1, &var2] – 通過值來捕獲 var1 ,引用來捕獲 var2 。

到現在,我們已經看到了我們可以防止一些變數被捕獲,防止通過值的關鍵字為const,防止通過引用的關鍵字non-const 。因此,在上面的捕獲類別中我們已經覆蓋了1,2 和4。捕獲constreference是不可能的(即,[const&a])。我們現在將會去研究一下最後一個在通過值呼叫模式的捕獲。

'mutable' 的說明

在引數說明括號後面,我們指定了一個mutable關鍵字。我們把所有通過值來捕獲的變數放入一個通過值呼叫模式。如果我們不放置一個mutable關鍵字,所有通過值來捕獲的變數都是隻讀的,你不能在匿名函式中修改它。放置一個關鍵字mutable就告訴編譯器強制拷貝所有通過值來捕獲的變數。因此你接下來可以修改通過值捕獲的變數了。 沒有那種方法能否選擇的通過值來捕獲const 或非const 變數。 或者簡單點,你可以假定他們通過傳參的形式傳入匿名函式。

例如:

int x=0,y=0,z=0;

 

[=]()mutable->void // ()是必須的, 當我們指定一個 'mutable'關鍵字時

{

    x++;

// 因為所有變數都是在通過值呼叫的模式下捕獲

// 編譯器會產生一個警告,由於 y,z 均未使用

}();

// x的值依然為0

匿名函式呼叫後,x的值仍然是0,因為僅僅只是對x的一個拷貝做了修改,而不是引用。編譯器僅對y和z 產生而不是先前定義的變數(a, b, c...)產生一個告警通常也是很有趣的一件事情。然而,它不會抱怨你是使用了預先定義的變數。智慧的編譯器——我不敢說會使什麼結果!

匿名函式與函式指標,函式物件有怎樣的區別呢?

函式的指標不保留狀態,而匿名函式保留。通過引用的捕獲,匿名函式可以在呼叫期間保留它們的狀態。函式卻不能。 函式指標不是型別安全的,他們是容易出錯的,我們必須嚴格遵守呼叫約定並且要求複雜的語法。

函式物件也儲存狀態。但是甚至一個小的應用程式,你必須寫一個類,並且在類中定義一些變數,並且過載操作符()。更重要的是,你必須在函式塊外面做這些工作以便於其他對於這個類應該呼叫operator()的函式必須知道他。這樣破壞了程式碼的的流程。

匿名錶達式的型別是什麼呢?

匿名錶達式其實就是一些類。 你能夠把它們儲存在一個function 類物件中。這個類,對於匿名錶達式,被定義在std::tr1名稱空間中。讓我們看一個例子:

#include<functional>

....

std::tr1::function<bool(int)>s IsEven = [](intn)->bool { returnn%2 == 0;};

...

IsEven(23);

tr1名稱空間在技術報告1中,技術報告1被C++0x 委員會使用,你可以自己去找更多的資訊。 <bool(int)>表示了對於function類的模板引數,意思是說:函式返回一個bool 型別,傳入一個int 型別。 基於匿名錶達式被放到函式物件中這樣的機制,你必須正確的進行型別轉換;否則,編譯器會因為型別不匹配而產生錯誤或警告。但是,就像你能夠看到的那樣,使用auto關鍵字會更加的方便。

這裡有一些用例,然而,你在哪裡必須使用一個function —— 當你需要穿過函式傳遞匿名錶達式時. 如下例:

using namespace std::tr1;

void TakeLambda(function<void(int)> lambda)

// 不能夠在函式引數中使用 'auto' 

{

    // 呼叫它!

    lambda(32);

}

 

// 在程式的某些地方 ... 

TakeLambda(DisplayIfEven);// 看上述程式碼 'DisplayIfEven'

DisplayIfEven 匿名錶達式 (或函式!) 接受 int, 返回空。  function 類以相同的方式被作為引數在TakeLambda中使用。進一步,它呼叫匿名錶達式, 匿名錶達式最終呼叫DisplayIfEven 匿名錶達式.

我已經簡化了 TakeLamba,此表示式應該為 (逐漸展示):

// 引用,不應當拷貝'函式'物件

void TakeLambda(function< void(int) > & lambda);

 

// 只讀的引用,不應該修改函式物件

void TakeLambda(const function< void(int) > &lambda);

 

// 完全限定名

void TakeLambda(const std::tr1::function< void(int) > &lambda);

在 C++中介紹匿名錶達式的目的是什麼?

匿名錶達式對於許多STL 函式時非常有用的——即函式要求一個函式指標或函式物件(使用operator()過載)。簡而言之,匿名錶達式對於要求回撥函式的程式歷程是非常有用的。起初,我沒去覆蓋STL 的函式,但是以一種簡單的容易理解的形式解釋了匿名錶達式的用處。非STL 的例子也許顯得很多餘,但卻能夠幫助澄清這個主題。 

例如, 下面的函式的引數要求傳入一個函式。 它將呼叫傳進的函式。函式指標,函式物件,或者匿名錶達式應該是需要的型別,返回一個void型別並且接受一個int型別的引數作為唯一的引數。 

void CallbackSomething(int nNumber, function<void(int)> callback_function)

{

    // 呼叫指定的 '函式'

    callback_function(nNumber);

}

這裡我以三種不同的方式呼叫 CallbackSomething 函式: 

// 函式

void IsEven(int n)

{

   std::cout <<((n%2 == 0) ? "Yes": "No");

}

 

// 類對操作符() 的過載

class Callback

{

public:

   void operator()(int n)

   {

      if(n<10)

         std::cout <<"Less than 10";

      else

         std::cout <<"More than 10";

   }

};                                      

 

int main()

{

   // 傳遞一個函式指著

   CallbackSomething(10,IsEven);

 

   // 傳遞一個函式物件

   CallbackSomething(23,Callback());

 

   // 另外一種方式..

   Callback obj;

   CallbackSomething(44,obj);

 

   // 本地定義的匿名錶達式!

   CallbackSomething(59,[](int n)   { std::cout << "Half: " <<n/2;}     );

}

好! 現在我想讓 類呢個能夠顯示一個數字是否大於N(替代一個常量10)。 我們可以這樣來完成這個任務:

class Callback

{

   /*const*/int Predicate;

public:

   Callback(intnPredicate) : Predicate(nPredicate) {}

 

   void operator()(int n)

   {

      if( n < Predicate)

         std::cout <<"Less than " << Predicate;

      else

         std::cout <<"More than " << Predicate;

   }

};

以便於能使得這樣可呼叫,我們需要一個整型常量來構造它。原始的函式CallbackSomething不需要被改變——它仍然能夠攜帶一個整型引數來呼叫程式歷程!這就是我們怎樣做它方法:

// 傳進一個函式物件

CallbackSomething(23, Callback(24));

// 24 是CallBack 建構函式的引數,不是CallbackSomething!的引數

 

// 另一種方式..

Callback obj(99); // 設定 99 去判斷

CallbackSomething(44, obj);

這個方式,我們使得Callback類擁有儲存狀態的能力。記住,只要物件在,它的狀態就保持。因此,如果你把一個 obj物件傳進多個CallbackSomething的呼叫(或者任何其他相似的函式),它將擁有相同的斷言(狀態)。 就像你所瞭解的那樣,這在函式指標中絕對是不可能的——除非我們對函式引入另外一個引數。但是,這樣做破壞了程式的整個結構。如果一個特殊的函式需要一個攜帶制定型別的可呼叫的函式,我們只能穿幾這個型別的函式。函式指標不能儲存狀態,因此在這類場景下不可用。

使用匿名錶達式對於這樣的情況有可能嗎?如前面提到的那樣,匿名錶達式能夠通過指定的捕獲儲存狀態。因此,的確,使用匿名錶達式能夠完成這樣包含狀態的功能。這裡是一個被修改的匿名錶達式,被儲存在auto變數中:

int Predicate = 40;

 

// 匿名錶達式被儲存在 ' 有狀態的' 變數中

auto stateful  = [Predicate](intn)

   {  if( n < Predicate)

           std::cout <<"Less than " << Predicate;

         else

           std::cout <<"More than " << Predicate; 

   };

 

CallbackSomething(59, stateful ); // Morethan  40

    

Predicate=1000; 

CallbackSomething(100, stateful); // Predicate NOT changed for lambda!

能保持狀態的匿名錶達式被本地的定義在一個函式中,與函式物件比較更加的簡潔,並且比函式指標要清晰多。現在它擁有一個狀態。因此第一次呼叫會列印出“More than 40”,第二次呼叫相同。

注意,斷言值是通過傳值呼叫(非易變的),因此修改原始值不會影響到它在匿名錶達式中的狀態。為了反映在匿名錶達式中對斷言的修改,我們僅僅需要通過引用來捕獲變數就OK。當我們改變匿名錶達式如下,第二個呼叫將列印“Less than 1000”。

auto stateful  = [&Predicate](intn) // 通過引用來 

這與在一個類中增加一個方法如 的機制很相似,這個類可以修改斷言的值(狀態)。請檢視VC++ 的部落格,如下連結,會談到匿名錶達式——類的對映。

使用STL

STL的for_each函式會在一個範圍內/集合中為每一個元素呼叫指定的函式,因為它使用模板,它能夠接受任何型別的資料型別作為他的引數。我們可以使用這個特性作為匿名錶達式的一個例子。更簡單的,我將使用基本的陣列,而不是向量或列表,例如下所示:

using namespace std;

    

int Array[10] = {1,2,3,4,5,6,7,8,9,10};

 

for_each(Array,&Array[10], IsEven);

 

for_each(Array, Array+10,[](int n){ std::cout <<n << std::endl;});

第一個呼叫呼叫IsEven函式,第二個呼叫呼叫匿名錶達式,此表示式在for_each函式中定義。它會把這兩個函式呼叫10次,因為陣列的範圍包含/指定了10個元素。對於for_each函式是完全相同的我不需要再重複它第二個引數了(哦!但還是說了)。

這是一個非常簡單的例子,for_each和匿名錶達式不需要寫一個函式或類的情況下就可以被利用來能顯示值。 值得肯定,匿名錶達式能被進一步擴充套件用來做額外的工作——如列印是個數是否為素數,或者計算求和(通過使用引用呼叫),或修改一個範圍的元素。

修改匿名錶達式的形參?

對,是的,你能做到它,我一直在討論通過引用來捕獲並做出修改,但是沒有涵蓋修改自身的引數。這個需求一直都沒有出現直到現在。為了這樣做,僅通過引用(或指標)接受匿名錶達式的引數:

Well, yes! You can do that. For long, I talked abouttaking captures by references and making modifications, but did not covermodifying the argument itself. The need did not arise till now. To do this,just take the lambda's parameter by reference (or pointer):

// 'n' 被通過引用傳遞 (與通過引用捕獲不相同!)

for_each(Array, Array+10,[](int& n){ n *= 4; });

以上 for_each 的呼叫會對陣列 Array 的每個元素乘以 4.

就像我解釋的怎樣利用for_each函式來使用匿名錶達式一樣,你可以使用其他<algorithm>函式如 transform,generate,remove_if等等來使用它。匿名錶達式不僅僅侷限於使用STL 功能。他們也能夠被很有效率的適用於任何你所需求的函式物件。你需要確保給他傳遞正確個數,型別的引數,檢查它是否需要引數修改和上述的需要。因為這個文件講的並不是STL 和模板,所以我不再進一步談論這個。

一個匿名錶達式不能被當作函式指標來使用

是的,很失望和迷惑吧,但是卻是事實!你不能夠把一個匿名錶達式當作引數傳進一個引數為函式指標的函式。儘管有樣本程式碼,讓我首先解釋我到底想要表達什麼:

// 定義一個函式指標型別,有一個int 型的引數

typedef void (*DISPLAY_ROUTINE)(int);

 

// 定義一個函式,以一個函式指標作為引數,

void CalculateSum(int a,int b, DISPLAY_ROUTINE pfDisplayRoutine)

{

   // 呼叫這個函式指標

   pfDisplayRoutine(a+b);

}

CalculateSum接收一個DISPLAY_ROUTINE的函式指標型別. 下面的程式碼可以工作,因為我們給他了一個函式指標:

void Print(int x)

{

  std::cout << "Sum is: " <<x;

}

 

int main()

{

   CalculateSum(500,300, Print);

}

但是下面的程式碼會有問題:

CalculateSum (10, 20, [](int n) {std::cout<<"sum is: "<<n;}); 

// C2664:'CalculateSum' : cannot convert parameter 3 from 

//        '`anonymous-namespace'::<lambda1>'to 'DISPLAY_ROUTINE'

為什麼? 因為匿名錶達式是物件導向的,他們實際就是類。編譯器對於匿名錶達式內部產生了一個類的模型。內部產生的類會對操作符()過載; 並會擁有一些資料成員(通過捕獲指定和易變指定來推斷)——這些也許是隻讀,引用,或常規的成員變數和類的填充。這樣的類不能夠被降級成一個常規的的函式指標。

先前的例子是怎樣執行的呢?

好,因為智慧的類std::function!看(上面)CallbackSomething實際把一個函式當成引數,而並不是函式指標。

      如for_each——這個函式不會傳遞std::function,而是使用模板來代替。他直接呼叫括號內傳遞引數。認真的理解簡化的實現:

template <class Iteartor, class Function>

void for_each(Iteartor first, Iterator, Function func)

// 忽略返回值和其他引數

  // 假設下面的呼叫是一個迴圈 

  // 'func' 可以使一個常規函式,或可以是一個類的物件,操作()符被過載

 

  func(first);

}

同理,其他的STL 函式,如,,等等,可以工作在三種情況下:函式指標,函式物件,匿名錶達式。

      因此,如果你計劃在API 函式中使用匿名錶達式,如SetTimer, EnumFontFamilies,等等——別再計劃了!即使強制型別轉換匿名錶達式(通過傳遞它的地址),也不會工作,程式會在執行時崩潰。

相關文章