C++11語言擴充套件:常規特性

christian發表於2014-01-15

本節內容:auto、decltype、基於範圍的for語句、初始化列表、統一初始化語法和語義、右值引用和移動語義、Lambdas、noexcept防止丟擲異常、constexpr、nullptr——一個指標空值常量、複製並再丟擲異常、內聯名稱空間、使用者自定義資料標識。

auto

推導

在這裡因為它的初始化型別我們將得到x的int型別。一般來說,我們可以寫

x的型別我們將會根據初始化表示式“ expression”的型別來自動推導。

當一個變數的型別很難準確的知道或者寫出的時候,用atuo通過初始化表示式的型別進行推導顯然是非常有用的。

參考:

在C++98裡我們必須這樣寫

當一個變數的型別取決於模板實參的時候不用auto真的很難定義,例如:

 

從T*U的表示式,我們人類的思維是很難理清tmp的型別的,但是編譯器肯定知道T和U經過了什麼特殊處理。

auto的特性在最早提出和應用時是有區別的:1984年初,Stroustrup在他的Cfront實現裡用到了它,但是是由於C的相容問題被迫拿來用的,這些相容的問題已經在C++98和C++99接受了隱性int時候消失了。也就是說,現在這兩種語言要求每一個變數和函式都要有一個明確的型別定義。auto舊的含義(即這是一個區域性變數)現在是違法的。標準委員會的成員們在數百萬行程式碼中僅僅只找到幾百個用到auto關鍵字的地方,並且大多數出現在測試程式碼中,有的甚至就是一個bug。

auto在程式碼中主要是作為簡化工具,並不會影響標準庫規範。

參考:

decltype

decltype(E)的型別(“宣告型別”)可用名稱或表達E來宣告。例如:

這個概念已經流行在泛型程式設計的標籤“typeof”很長一段時間,但實際使用的“領域”的實現是不完整和不相容,所以標準版命名了decltype。

注意:喜歡使用auto時,你只是需要一個變數的型別初始化。如果你需要一個型別不是一個變數,那麼你需要用到decltype,例如返回型別。

參考:

  • the C++ draft 7.1.6.2 Simple type specifiers
  • [Str02] Bjarne Stroustrup. Draft proposal for “typeof”. C++ reflector message c++std-ext-5364, October 2002. (original suggestion).
  • [N1478=03-0061] Jaakko Jarvi, Bjarne Stroustrup, Douglas Gregor, and Jeremy Siek: Decltype and auto (original proposal).
  • [N2343=07-0203] Jaakko Jarvi, Bjarne Stroustrup, and Gabriel Dos Reis: Decltype (revision 7): proposed wording.

基於範圍的for迴圈

宣告的範圍像是STL-sequence定義的begin()和end(),允許你在這個範圍內迴圈迭代。所有標準容器可以作為一個範圍來使用,比如可以是std::string,初始化器列表,一個陣列,和任何你可以定義begin()和end()的,比如istream。例如:

你可以看到,V中所有的元素都從begin()開始迭代迴圈到了end()。另一個例子:

begin()(和end())可以被做為x.begin()的成員或一個獨立的函式被稱為開始(x)。成員版本優先。

參考:

初始化列表

推導

初始化列表不再只針對於陣列了。定義一個接受{}初始化列表的函式(通常是初始化函式)接受一個std::initializer_list < T >的引數型別,例如:

初始化器列表可以是任意長度的,但必須同種型別的(所有元素必須的模板引數型別,T,或可轉換T)。

一個容器可能實現一個初始化列表建構函式如下:

直接初始化和複製初始化的區別是對初始化列表的維護,但是因為初始化列表的相關聯的頻率就降低了。例如std::vector有一個int型別顯示建構函式和initializer_list建構函式:

函式可以作為一個不可變的序列訪問initializer_list。例如:

僅具有一個std::initializer_list的單引數建構函式被稱為初始化列表建構函式。

標準庫容器,string型別及正規表示式均具有初始化列表建構函式,以及(初始化列表)賦值函式等。一個初始化列表可被用作Range,例如,表示式Range。

初始化列表是一致泛化初始化解決方案的一部分。他們還防止型別收窄。一般來說,你應該通常更喜歡使用{ }來代替()初始化,除非你想用c++98編譯器來分享程式碼或(很少)需要使用()呼叫沒initializer_list過載建構函式。

參考:

統一初始化語法和語義

c++ 98提供了幾種方法初始化一個物件根據其型別和初始環境。濫用時,會產生可以令人驚訝的錯誤和模糊的錯誤訊息。推導:

要記得初始化的規則並選擇最好的方法去初始化是比較難的。

C++11的解決方法是允許所有的初始化使用初始化列表

重點是,x{a}在在執行程式碼中都建立了一個相同的值,所以在使用“{}”進行初始化合法的情況下都產生了相同的結果。例如:

參考:

右值引用和移動語義

左值(用在複製操作符左邊)和右值(用在複製操作符右邊)的區別可以追溯到Christopher Strachey (C++遙遠的祖先語言CPL和外延語義之父)的時代。在C++中,非const引用可以繫結到左值,const引用既可以繫結到左值也可以繫結要右值。但是右值卻不可以被非const繫結。這是為了防止人們改變那些被賦予新值之前就被銷燬的臨時變數。例如:

如果incr(0)被允許,那麼就會產生一個無法被人看到的臨時變數被執行增加操作,或者更糟的0會變成1.後者聽起來很傻,但實際上確實存在這樣一個bug在Fortran編譯器中:為值為0的記憶體位置分配。

到目前為止還好,但考慮以下程式碼:

如果T是一個複製元素要付出昂貴代價的型別,比如string和vector,swap將會變成一個十分昂貴的操作(對於標準庫來說,我們有專門化的string和vector來處理)。注意一下這些奇怪的現象:我們並不想任何變數拷貝。我們僅僅是想移動變數a,b和tmp的值。

在C++11中,我們可以定義“移動建構函式”和“移動賦值操作符”來移動,而不是複製他們的引數:

& &表明“右值引用”。一個右值引用可以繫結到一個右值(而不是一個左值):

賦值這個操作的背後思想,並不是拷貝,它只是構造一個源物件的代表然後再替換。例如,string s1 = s2的移動,它不是產生s2的拷貝,而是讓s1把s2中字元變為自己的同時刪除自己原有的字串(也可以放在s2中,但是它也面臨著被銷燬)

我們如何知道是否可以簡單的從源物件進行移動?我們可以告訴編譯器:

move(x)只是意味著你“你可以把x當作一個右值”,

如果把move()稱做eval()也許會更好,但是現在move()已經用了好多年了。在c++11中,move()模板(參考簡介)和右值引用都可以使用。

右值引用也可以用來提供完美的轉發。

在C++0x的標準庫中,所有的容器都提供了移動建構函式和移動賦值操作符,那些插入新元素的操作,如insert()和push_back(), 也都有了可以接受右值引用的版本。最終結果是,在無使用者干預時,標準容器和演算法的效能都提升了,因為複製操作的減少。

參考:

lambdas

Lambda表示式是一種描述函式物件的機制,它的主要應用是描述某些具有簡單行為的函式(譯註:Lambda表示式也可以稱為匿名函式,具有複雜行為的函式可以採用命名函式物件,當然,簡單和複雜之間的劃分依賴於程式設計人員的選擇)。例如:

引數 [&](int a, int b) { return abs(a) < abs(b); }是一個”lambda”(又稱為”lambda函式”或者”lambda表示式”), 它描述了這樣一個函式操作:接受兩個整形引數a和b,然後返回對它們的絕對值進行”<“比較的結果。(譯註:為了保持與程式碼的一致性,此處應當為”[] (int a, int b) { return abs(a) < abs(b); }”,而且在這個lambda表示式內實際上未用到區域性變數,所以 [&] 是無必要的)

一個Lambda表示式可以存取在它被呼叫的作用域內的區域性變數。例如:

有人認為這“相當簡潔”,也有人認為這是一種可能產生危險且晦澀的程式碼的方式。我的看法是,兩者都正確。

[&] 是一個“捕捉列表(capture list)”,用於描述將要被lambda函式以引用傳參方式使用的區域性變數。如果我們僅想“捕捉”引數v,則可以寫為: [&v]。而如果我們想以傳值方式使用引數v,則可以寫為:[=v]。如果什麼都不捕捉,則為:[]。將所有的變數以引用傳遞方式使用時採用 [&], [=] 則相應地表示以傳值方式使用所有變數(譯註:“所有變數”即指lambda表示式在被呼叫處,所能見到的所有區域性變數)。

如果某一函式的行為既不通用也不簡單,那麼我建議採用命名函式物件或者函式。例如,如上示例可重寫為:

對於簡單的函式功能,比如記錄名稱域的比較,採用函式物件就略顯冗長,儘管它與lambda表示式生成的程式碼是一致的。在C++98中,這樣的函式物件在被用作模板引數時必須是非本地的(譯註:即你不能在函式物件中像此處的lambda表示式那樣使用被呼叫處的區域性變數),然而在C++中(譯註:意指C++0x),這不再是必須的。

為了描述一個lambda,你必須提供:

  • 它的捕捉列表:它可以使用的變數列表(除了形參之外),如果存在的話(”[&]” 在上面的記錄比較例子中意味著“所有的區域性變數都將按照引用的方式進行傳遞”)。如果不需要捕捉任何變數,則使用 []。
  • (可選的)它的所有引數及其型別(例如: (int a, int b) )。
  • 組織成一個塊的函式行為(例如:{ return v[a].name < v[b].name; })。
  • (可選的)採用了新的字尾返回型別符號的返回型別。但典型情況下,我們僅從return語句中去推斷返回型別,如果沒有返回任何值,則推斷為void。

參考:

noexcept防止丟擲異常

如果一個函式不能丟擲異常或者一個程式沒有對函式丟擲的異常進行處理,那麼這個函式可以用關鍵字noexcept進行修飾,例如:

如果一個被noexcept修飾的函式丟擲了異常(所以異常會跳出唄noexcept修飾的函式),程式會呼叫std::terminate()這個函式來終止程式。在物件被明確定義的狀態下不能呼叫terminate();比如無法保證解構函式正常呼叫,不能保證棧的自動釋放,也無法保證在遇到任何問題時重新啟動。故意這樣的使noexcept成為一種簡單“粗暴”而有效的處理機制-它比舊的處理機制throw()動態丟擲異常要有效的多。

它可以讓一個函式根據條件來實現noexcept修飾。比如,一個演算法可以根據他的模板引數來決定自己是否丟擲異常。

這裡,第一個noexcept被用作操作符operator:如果if f(v.at(0))不能夠丟擲異常,noexcept(f(v.at(0)))則返回true,所以f()和at()是無法丟擲異常noexcept。

noexcept()操作符是一個常量表示式,並且不計算表示式的值。

宣告的通常形式是noexcept(expression),並且單獨的一個“noexcept”關鍵字實際上就是的一個noexcept(true)的簡化。一個函式的所有宣告都必須與noexcept宣告保持 相容。

一個解構函式不應該丟擲異常;通常,如果一個類的所有成員都擁有noexcept修飾的解構函式,那麼這個類的解構函式就自動地隱式地noexcept宣告,而與函式體內的程式碼沒有關係。

通常,將某個丟擲的異常進行移動操作是一個很壞的主意,所以,在任何可能的地方都用noexcept進行宣告。如果某個類的所有成員都有使用noexcept宣告的解構函式,那麼這個類預設生成的複製或者移動操作(類的複製建構函式,移動建構函式等)都是隱式的noexcept宣告。(?)

noexcept 被廣泛地系統地應用在C++11的標準庫中,以此來提供標準庫的效能和滿足標準庫對於簡潔性的需求。

參考:

constexpr

常量表示式機制:

  • 提供了更多的通用的值不發生變化的表示式
  • 允許使用者自定義的型別成為常量表示式
  • 提供了一種保證在編譯期完成初始化的方法

考慮下面這段程式碼:

在這裡,常量表示式關鍵字constexpr表示這個過載的操作符“|”就應該像一個簡單的表單一樣,如果它的引數本身就是常量 ,那麼這個操作符應該在編譯時期就應該計算出它的結果來。
除了可以在編譯時期被動地計算表示式的值之外,我們希望能夠主動地要求表示式在編譯時期計算其結果值,從而用作其它用途,比如對某個變數進行賦值。當我們在變數宣告前加上constexpr關鍵字之後,可以實現這一功能,當然,它也同時會讓這個變數成為常量。

通常,我們希望編譯時期計算可以保護全域性或者名字空間內的物件,對名字空間內的物件,我們希望它儲存在只讀空間內。
對於那些建構函式比較簡單,可以成為常量表示式(也就是可以使用constexpr進行修飾)的物件可以做到這一點(?)

  •  const的主要功能是修飾一個物件而不是通過一個介面(即使物件很容易通過其他介面修改)。只不過宣告一個物件常量為編譯器提供了優化的機會。特別是,如果一個宣告瞭一個物件常量而他的地址沒有取到,編譯器通常可以在編譯時對他進行初始化(儘管這不是肯定的)保證這個物件在他的列表裡而不是把它新增到生成程式碼裡。
  • constexpr的主要功能可以在編譯時計算表示式的值進行了範圍擴充套件,這是一種計算安全而且可以用在編譯時期(如初始化列舉或者整體模板引數)。constexpr宣告物件可以在初始化編譯的時候計算出結果來。他們基本上只儲存在編譯器的列表,如果需要的話會釋放到生成的程式碼裡。

參考:

nullptr 一個指標空值常量

nullptr是一個指標空值常量,不是一個整數。

參考:

複製並再丟擲異常

你如何捕獲一個異常然後把它丟擲到另一個執行緒?使用標準文件18.8.5裡描述的標準庫的魔力方法吧。

exception_ptr current_exception(); 正在處理的異常(15.3)或者正在處理的異常的副本(拷貝)將返回一個exception_ptr 變數,如果當前沒有遇到異常,返回值為一個空的exception_ptr變數。只要exception_ptr指向一個異常,那麼至少在exception_ptr的生存期內,執行時能夠保證被指向的異常是有效的。

void rethrow_exception(exception_ptr p);
template exception_ptr copy_exception(E e); 它的作用如同:

當我們需要將異常從一個執行緒傳遞到另外一個執行緒時,這個方法十分有用的。

內聯名稱空間

內聯名稱空間機制是通過一種支援版本更新的機制來支援庫的演化,推導:

我們這裡有一個名稱空間Mine包含最新版本的(V99)和前一個版本(V98),如果你想要顯式應用(某個版本的函式),你可以:

內聯的關鍵是使內聯名稱空間的宣告和直接在外圍名稱空間宣告一樣。

lnline是靜態的及面向實現的設施,它由名稱空間的設計者放置來幫助使用者進行選擇。對於Mine的用是不可以說“我想要的名稱空間是V98而不是V99”。

參照:

  • Standard 7.3.1 Namespace definition [7]-[9].

使用者自定義資料標識

C++提供了很多內建的資料識別符號(2.14節變數)

built-in types (2.14 Literals):

然而,愛C++98裡並沒有使用者自定義的資料識別符號。這就有悖於甚至衝突“使用者自定義型別和內建leiixng一樣得到支援”的原則。特殊情況下,人們有這樣的需求:

C++11支援“使用者自定義資料標識”通過在變數名後面加一個字尾來標定所需型別,例如:

注意constexpr的使用可以在編譯時期計算。有了這個功能,我們可以這樣寫:

基本(實現)方法是編譯器在解析什麼語句代表一個變數之後,再分析一下字尾。使用者自定義資料標識機制只是簡簡單單的允許使用者制定一個新的字尾,並決定如何對它之前的資料進行處理。要想重新定義一個內建的資料標識的意義或者它的引數、語法是不可能的。一個資料標識操作符可以使用它(前面)的資料標識傳遞過來的處理過的值(如果是使用新的沒有定義過的字尾的值)或者沒有處理過的值(作為一個字串)。
要得到一個沒有處理過的字串,只要使用一個單獨的const char*引數即可,例如:

這個C語言風格的字串”1234567890123456789012345678901234567890″被傳遞給了操作符 operator”” x()。注意,我們並沒有明確地把數字轉換成字串。

有以下四種資料標識的情況,可以被使用者定義字尾來使用使用者自定義資料標識:

  • 整型標識:允許傳入一個unsigned long long或者const char*引數
  • 浮點型標識:允許傳入一個long double或者const char*引數
  • 字串標識:允許傳入一組(const char*,size_t)引數
  • 字元標識:允許傳入一個char引數。

注意,你為字串標識定義的標識操作符不能只帶有一個const char*引數(而沒有大小)。例如:

根本原因是如果我們想有一個“不同的字串”,我們同時也想知道字元的個數。字尾可能比較短(例如,s是字串的字尾,i是虛數的字尾,m是米的字尾,x是擴充套件型別的字尾),所以不同的用法很容易產生衝突,我們可以使用namespace(名稱空間)來避免這些名字衝突:

參考:

相關文章