C++11语言新特性-《C++标准库(第二版)》读书笔记
文章目錄
- 3.1.1 微小但重要的語法提升
- Template 表達式內的空格
- nullptr 和std::nullptr_t
- 3.1.2 以auto 完成自動類型推導
- 3.1.3 一致性初始化(Uniform Initialization) 與初值列(Initializer List)
- 3.1.4 Range-Based for 循環
- 3.1.5 Move 語義和Rvalue Reference
- Rvalue和Lvalue Reference 的重載規則
- 返回Rvalue Reference
- 3.1.6 新式的字符串字面常量(String Literal)
- Raw String Literal
- 3.1.7 關鍵字noexcept
- 3.1.8 關鍵字constexpr
- 3.1.9 嶄新的Template 特性
- Variadic Template
- Alias Template(帶別名的模板,或者叫Template Typedef)
- 其他的Template新特性
- 3.1.10 Lambda
- Lambda的語法
- Capture(用以訪問外部作用域)
3.1.1 微小但重要的語法提升
Template 表達式內的空格
“在兩個template表達式的閉符之間放一個空格” 的要求已經過時了
vector<list<int> >; //OK in each C++ version vector<list<int>>; // OK since C++11上述兩種形式都可以現在。
nullptr 和std::nullptr_t
C++11允許使用nullptr取代0或者NULL,用來表示一個pointer(指針)指向所謂的 no value (此不同于擁有一個不確定值) 。 這個新特性能幫助我們在“null pointer被解釋為一個整數值” 時避免誤解。
例如
void f(int); void f(void *);f(0); // calls f(int) f(NULL); // calls f(int) if NULL is 0 ,ambiguous otherwisef(nullptr); // calls f(void*)nullptr 是新關鍵字。它被自動轉換為各種pointer類型,但不會被轉換為任何整數類型。 它擁有類型std::nullptr_t ,定義于< cstddef>, 所以現在甚至可以重載函數令它們接受 null pinter. 注意, std:: nullptr_t 被視為一個基礎類型。
3.1.2 以auto 完成自動類型推導
C++11 允許聲明一個變量或對象(Object)而不需要指明其類型,只需要說它是auto 。例如
auto i =42; // i has type intdouble f(); // auto d=f(); // d has type double以auto聲明的變臉,其類型會根據其初值被自動推導出來,因此一定需要一個初始化操作:
auto i; //ERROR : can't deduce the type of i可為它加上額外的限定符,例如
static auto vat =0.19;如果類型很長或表達式很復雜,auto特別有用,例如
vector<stirng> v;auto pos = v.begin(); // pos has type vector<string> :: iteratorauto l = [] (int x) -> bool { // l has the type of a lambda..., //taking an int and returning a bool };上述末尾那個奇怪的東西是個對象,表示一個lambda。
3.1.3 一致性初始化(Uniform Initialization) 與初值列(Initializer List)
C++11引入了“一致性初始化(uniform initialization)”的概念, 意思是面對任何初始化動作,你可以使用相同語法,也就是使用大括號。以下皆成立:
int values [] {1,2,3}; std::vector<int> v{2,3,5,7,11,13,17};std::vector<std::string> cities{"Berlin","New York","London","Braunschweig","Cairo","Cologne" };std::complex<double> c{4.0, 3.0 }; //equivalent to c(4.0 , 3.0)初始列(initializer list) 會強迫造成所謂value initializaiton ,意思是即使某個local 變量屬于某種基礎類型(那通常是不明確的初值)也會被初始化為0(或者nullptr----如果它是個pointer的話):
int i // i has undefined value int j{}; // j is initialized by 0int* p; // p has undefined value int* q{}; // q is initialized by nullptr然而請注意,窄化(narrowing) -----也就是精度降低或造成數值變動----- 對大括號而言是不可成立的。例如:
int x1(5.3) ; // OK,but OUCH:x1 becomes 5 int x2 =5.3; // OK,but OUCH: x2 becomes 5int x3{5.0}; //ERROR:narrowing int x4={5.3}; //ERROR :narrowingchar c1{7}; //OK:even though 7 is an int , this is not narrowingchar c2{99999};// ERROR:narrowing(if 99999 doesn't fit into a char)std::vector<int> v1{1,2,4,5}; //OKstd::vector<int> v2{1,2.3, 4, 5.6}; // ERROR:narrowing doubles to ints為了支持“用戶自定義類型之初值列”的概念, C++11提供了class template std:: initializer_list<>,用來支持以一系列值(a list if values) 進行初始化,或在“你想要處理一系列值(a list of values)” 的任何地點進行初始化,例如:
void print( std::initializer_list<int> vals){for(auto p=vals.begin(); p!=vals.end();++p)std::cout<< *p <<<<endl; }print({ 12, 3, 5, 7, 11, 13, 15}); //pass a list of values to print()當“指明實參個數” 和“指明一個初值列” 的構造函數(ctor)同時存在,嗲有初值列的那個版本勝出:
class P {public:P(int ,int );P(std:: initialier_list<int>); };P p(77, 5); //calls P::P(int,int )P q{77 , 5}; // calls P::P (intializer_list)P r{77,5,42};//calls P::P (intializer_list)P s={77,5};//calls P::P (intializer_list)如果上述“帶有1個初值列”的構造函數不存在,那么接受2個int的那個構造函數會被調用以初始化q和s,而r的初始化將無效。
由于初值列的關系,explicit之于“接受一個以上實參”的構造函數也會變得不一樣。如今你可以令“多數值自動類型轉換”不再起作用,即使初始化以=語法進行:
class P{public:P(int a,int b){...}explicit P(int a,int b,int c){...}};P x(77 , 5); //OK P y{77,5};//OK P z {77,5,42}; //OK P v={77,5};//OK(implicit type conversion allowed) P w={77,5,42};// ERROR due to explicit(no implicit type conversion allowed)同樣地,explicit構造函數如果接受的是個初值列,會失去“初值列帶有0個、1個或多個初值”的隱式轉換能力。
void fp(const P&);fp({47,11});//OK,implicit conversion of{47,11} into P fp({47,11,3});//ERROR due to explicit fp(P{47,11});//OK,explicit conversion of {47,11}into P fp(P{47,11,3}); //OK,explicit conversion of {47,11,3} into P3.1.4 Range-Based for 循環
C++11 引入了一種嶄新的for循環形式,可以逐一迭代某個給定的區間、數組、集合(range,array,or collection)內的每一個元素。 其他編程語言可能稱此為foreach循環。其一般性語法如下:
for(decl:coll){statement }其中decl是給定的coll集合的每個元素的聲明;針對這些元素,給定的statement會被執行。例如下面針對傳入的初值列的每個元素,調用給定的語句,于是在標準輸出裝置cout輸出元素值:
for( int i: {2,3,5,7,9,11,13,15}){std::cout<<i <<std::endl; }如果要將vector vec的每個元素elem乘以3,可以這么做:
std::vector<double> vec; ... for( auto & elem: vec){elem*=3; }這里“聲明elem為一個reference”很重要,若不這樣做,for循環中的語句會作用在元素的一份local copy身上。
這意味著,為了避免調用每個元素的copy構造函數和析構函數,你通常應該聲明當前元素為一個const reference。于是一個用來“打印某集合內所有元素”的泛型函數應該寫成這樣
template<typename T> void printElements(const T& coll){for(const auto& elem:coll){std::cout<<elem<<std::endl;} }在這里,這種所謂range-based for 語句等同于:
{for(auto _pos=coll.begin(); _pos != coll.end(); ++_pos){const auto &elem =*_pos;std::cout<<elem<< std::endl;} }一般而言,如果coll提供成員函數begin()和end(),那么一個range-based for 循環聲明為
for (decl: coll){statement }便等同于:
for( auto _pos=coll.begin(),_end=coll.end(); _pos!=_end; ++_pos){decl= *_pos;statement }或者如果不滿足上述條件,那么也等同于一下使用一個全局性begin()和end()且兩者都接受coll為實參:
{for(auto _pos=begin(coll),_end=end(coll); _pos!=_end;++_pos){decl=*_pos;statement}}于是,你甚至可以針對初值列(initializer list)使用range-based for循環,因為class template std::initializer_list<>提供了成員函數begin()和end().
此外還有一條規則,允許你使用尋常的、大小已知的C-style array,例如:
int array[]={1, 2, 3, 4, 5};long sum=0; //process sum of all elementsfor(int x:array)sum+=x;for(auto elem: {sum,sum*2, sum*4})std::cout<< elem <<std::endl;產生如下輸出:
當元素在for循環中被初始化為decl,不得有任何顯式類型轉換(explicit type conversion) 。因此下面的代碼無法通過編譯:
class C{public:explicit C(const std::string& s);// explicit(!) type conversion from strings };std::vector<std::string> vs; for(const C& elem:vs){ // ERROR,no conversion from string to C definedstd::cout<<elem<<std::endl; }3.1.5 Move 語義和Rvalue Reference
C++11的一個最重要的特性就是,支持move semantic(搬遷語義)。這項特性更進一步進入了C++11主要設計目標內,用以避免非必要拷貝(copy)和臨時對象(temporary)。
這項新特性十分復雜,這里盡量給出一份簡明扼要的介紹和摘要。
考慮以下代碼
void createAndInsert(std::set<X>& coll){X x;// create an object of type Xcoll.insert(x); //insert it into the passed collection}在這里我們將新對象插入集合(collection)中,后者提供了一個成員函數可為傳入的元素建立一份內部拷貝(internal copy):
namespace std{template<typeneme T,..> class set{public:...insert(const T &v);// copy value of v}; }這樣的行為是有用的,因為集合(collection)提供value semantic 及安插“臨時對象”(temporary object) 或“安插后會被使用或被改動的對象”的能力:
X x; coll.insert(x); //inserts copy of xcoll.insert(x+x); //inserts copy of temporary rvaluecoll.insert(x); //insert copy of x (although x is not used any longer)然而,對于后兩次x安插動作,更好的是指出“被傳入值(也就是x+x 的和以及x)不再被調用者使用”,如此一來coll內部就不需為它建立一份copy且“以某種方式move其內容進入新建元素中”。當x的復制成本高昂的時候—例如它是個巨大的string集合—這會帶來巨大的效能提升。
自C++11開始,上述行為成為可能,然而程序員必須自行指明“move是可行的,除非被安插的那個臨時對象還會被使用”。雖然編譯器自身也有可能找出這個情況,允許程序員執行這項工作畢竟可使這個特性被用于邏輯上任何適當之處。先前的代碼只需要簡單改成這樣:
X x; coll.insert(x); //inserts copy of x(OK,x is still used)coll.insert(x+x); //moves(or copies) contents of temporary rvaluecoll.insert(std::move(x)); //moves(or copies) contents of x into coll有了聲明于< utility >的std::move() ,x可被moved而不再被copied 。然而,std::move() 自身并不做任何moving工作,它只是將其實參轉成一個所謂的rvalue reference ,那是一種被聲明為 X&&的類型。 這種新類型主張rvalue(不具名的臨時對象只能出現在賦值操作的右側)可被改動內容。 這份契約的含義是:這是個不再被需要的(臨時)對象,所以你可以“偷”其內容和/或其資源。
現在,我們讓集合提供一個insert()重載版本,用以處理這些rvalue reference:
namespace std{template<typename T,...> class set{public:...insert(const T& x); //for lvalues:copies the value...insert(T &&x); // for rvalues:moves the value...};}我們可以優化這個針對rvalue reference 的重載版本,令它“偷取”x的內容。 為了這么做,需要type of x的幫助,因為只有type of x 擁有接近本質的機會和權利。 舉個例子,你可以運用internal array 和pointer of x 來初始化被安插的元素。如果class x 本身是個復雜類型,原本你必須為它逐一復制元素,現在這么做則會帶來巨大的效能改善。欲初始化新的內部元素,我們只需調用class X提供的一個所謂move構造函數,它“偷取”傳入的實參值,初始化一個新元素。所有復雜類型都應該提供這樣一個特殊構造函數 ----C++標準庫也提供了-----永安里將一個既有元素的內容搬遷(move)到新元素中:
class X{public:X(const X& lvalue); //copy constructorX(X&& rvalue); //move constructor}舉個例子,string 的move構造函數只是將既有的內部字符數組(existing internal character array )賦予(assign)新對象,而非建立一個新arrry然后復制所有元素。 同樣情況也適用于所有集合class:不再為所有元素建立一份拷貝,只需將內部內存(internal memory)賦予新對象就行。如果move構造函數不存在,copy構造函數就會被用上。
零位,你必須確保對于被傳對象(其value被"偷取")的任何改動—特別是析構----都不至于沖擊新對象(如今擁有value)的狀態。因此,你往往必須清除被傳入的實參的內容,例如將nullptr賦值給“指向了容器元素”的成員函數。
將“move semantic 被調用”的對象的內容清除掉,嚴格來說并非必要,但不這樣做的話會造成整個機制幾乎失去用途。事實上,一般而言,C++標準庫的class保證了,在一次move之后,對象處于有效但不確定的狀態。也就是說,你可以在move之后對它賦予新值,但當前值是不確定的。STL容器則保證了,被搬移內容者,搬移后其值為空。
同樣地,任何nontrivial class都該同時提供一個copy assignment和一個move assignment 操作符:
class X{public:X& operator=(const X& lvalue); //copy assignment operatorX& operator=(X&& rvalue); // move assignment operator};對于string和集合,上述操作符可以簡單交換(swapping)內部內容和資源就好。然而你也應該清除*this 的內容,因為這個對象可能持有資源(如lock),因而最好很快釋放它們。 再強調一次,move semantic 并不要求你那么做,但這是C++標準庫容器所提供的一種優越質量。
最后,請注意兩個相關議題:1)rvalue和lvalue reference 的重載規則;2)返回rvalue reference.
Rvalue和Lvalue Reference 的重載規則
Rvalue和lvalue的重載規則(overloading rule)如下:
如果你只實現 void foo(X&);而沒有實現void foo(X&&); 行為如同C++98:foo() 可以lvalue但不能因rvalue被調用。
如果你實現void foo(const X&); 而沒有實現void foo(X&&); 行為如同C++98:foo()可以lvalue也可因rvalue被調用。
如果你實現void foo(X&); void foo(X&&); 或void foo(const X&); void foo(X&&); 你可以區分“為rvalue服務”和“為lvalue服務”.為“rvalue服務”的版本被允許且應該提供move語義。也就是說,它可以“偷取”實參的內部狀態和資源。
如果你實現void foo(X&&); 但既沒有實現 void foo(X&);也沒有實現void foo(const X&); ,foo()可因rvalue被調用,但當你嘗試以lvalue調用它,會觸發編譯錯誤。 因此,這里只提供move語義。這項能力被程序庫中諸如unique pointer ,file stream 或string stream 所用。
以上意味著,如果class未提供move語義,只提供慣常的copy構造函數和copy assignment 操作符,rvalue reference 可以調用它們。 因此std::move ()意味著"調用move語義(若有提供的話),否則調用copy語義"。
返回Rvalue Reference
你不需要也不應該move()返回值。C++ standard指出,對于以下代碼
X foo(){X x;return x; }保證有以下行為:
- 如果X有一個可取用的copy或move構造函數,編譯器可以選擇略去其中的copy版本。這也就是所謂的return value optimization(RVO),這個特性甚至在C++11之前就獲得了大多數編譯器的支持。
- 否則,如果X有一個move構造函數,X就被moved(搬移)。
- 否則,如果X有一個copy構造函數,X就被copied(復制).
- 否則,報出一個編譯期錯誤(compile-time error)。
也請注意,如果返回的是個Local nonstatic 對象,那么返回其rvalue reference 是不對的:
X&& foo(){X x;...return x;//ERROR:returns reference to nonexisting object }是的,rvalue reference 也是個reference ,如果返回它而它指向local對象,意味著你返回一個reference 卻指向一個不再存在的對象。是否對它使用std::move()倒是無關緊要。
3.1.6 新式的字符串字面常量(String Literal)
自C++11起,你可以定義raw string 和multibyte/wide-character 等字符串字面常量。
Raw String Literal
Raw string允許我么能定義字符序列(character sequence),做法是確切寫下其內容使其成為一個raw character sequence。于是你可以省下很多用來裝飾特殊字符的escape符號。
Raw string 以R"(開頭,以)" 結尾,可以內含line break。例如一個用來表示“兩個反斜線和一個n”的尋常字面常量可定義如下:
"\\\\n"也可定義它為如下raw string literal:
R"(\\n)"如果要在raw string 內寫出)”,可使用定義符(delimiter)。因此,一個raw string 的完整語法是R"delim(…)delim",其中delim是個字符序列,最多16個基本字符,不可含反斜線(backslash),空格(whitespace)和小括號(parenthesis)。
舉個例子,下面的raw string literal
R"nc(a\b\nc()")nc";等同于以下的尋常string literal:
“a\\\ n b\\nc()\” \n "這樣的string內含1個a、1個反斜線,一個新行字符(newline),若干空格,一個b,一個反斜線,一個n,一個c,一對小括號,一個雙引號,一個新行字符,以及若干空格。
定義正則表達式時raw string literal 特別有用。
3.1.7 關鍵字noexcept
C++11提供了關鍵字noexcept,用來指明某個函數無法----或不打算----拋出異常。例如
void foo() noexcept;聲明了foo()不打算拋出異常。 若有異常未在foo()內被處理—亦即如果foo()拋出異常—程序會被終止,然后std::terminate()被調用并默認調用std::abort() 。
3.1.8 關鍵字constexpr
自C++11起,constexpr可用來讓表達式核定于編譯期。例如
constexpr int square(int x){return x*x; }float a[square(9)]; //OK since C++11 :a has 81 elements這個關鍵字修正了一個在C++98 使用數值極限時出現的問題,在C++11以前,以下式子
std::numeric_limits<short>::max()無法被用作一個整型常量,雖然它在功能上等同于宏INT_MAX 。如今,在C++11中,這樣一個式子被聲明為constexpr,于是,舉個例子,你可以用它聲明array或進行編輯期運算(所謂metaprogramming):
std:;array<float,std::numeric_limits<short>::max()> a;3.1.9 嶄新的Template 特性
Variadic Template
自C++11開始,template可擁有那種“得以接受個數不定的template實參”的參數。 此能力稱為variadic template 。舉個例子,你可以寫出這樣的print(),得以在調用它時給予不定個數的實參且各具不同的類型:
void print(){ }template <typename T,typename..Types> void print(const T&firstArg, const Types&... args){std::cout<<firstArg<<std::endl; //print first argumentprint(args...); //call print() for remaining arguments }如果傳入1或多個實參,上述的function template 就會被調用,它會把第一實參區分開來,允許第一實參被打印,然后遞歸調用print()并傳入其余實參。你必須提供一個non-template重載函數print(),才能結束整個遞歸動作。
舉個例子,如下調用:
print(7.5, "hello", std::bitset<16>(377),42);會導致以下輸出
7.5 hello 0000000101111001 42注意,目前還在討論以下例子是否也有效,原因是,正規而言,一個“單實參的variadic形式”與一個“單實參的nonvariadic形式”形成歧義;然而編譯器通常接受這樣的代碼:
template <typename T> void print(const T&arg){std::cout<<arg<<std::endl; }template<typename T,typename ..Types>void print(const T& firstArg,const Types&... args){std::cout<<firstArg <<std::endl;//print first argumentprint(args...); //call print() for remaining arguments }在variadic template 內,sizeof…(args)會生成實參個數。
Class std:: tuple<> 大量使用了這一特性。
Alias Template(帶別名的模板,或者叫Template Typedef)
自C++11 起,支持template(partial) type definition 。然而由于關鍵字typename 用于此處時總是出于某種原因而失敗,所以這里引入關鍵字using ,并因此引入一個新術語alias template 。舉個例子,寫出如下代碼
template<typename T> using Vec=std::vector<T,MyAlloc<T>>;//standard vector using own allocator之后,Vec< int > coll; 就等價于std::vector<int,MyAlloc< int>> coll;
其他的Template新特性
自C++11起,function template可擁有默認的template實參。此外,local type可被當作template實參。
3.1.10 Lambda
C++11引入了lambda,允許inline函數的定義式被用作一個參數,或是一個local對象。
Lambda改變了C++標準庫的用法。
Lambda的語法
所謂lambda是一份功能定義式,可被定義于語句(statement)或表達式(expression)內部。因此你可以拿lambda當作inline函數使用。
最小型的lambda函數沒有參數,什么也不做,如下:
[]{std::cout<< "hello lambda" <<std::endl; }可以直接調用它:
[]{std::cout<<"hello lambda" <<std::endl; }(); //prints "hello lambda"或者把它傳遞給對象,使之能被調用:
auto l=[]{std::cout<<"hello lambda"<<std::endl;};l(); //prints “hello lambda”如你所見,lambda總是由一個所謂的lambda introducer引入:那是一組方括號,你可以在其內指明一個所謂的capture,用來在lambda內部訪問”nonstatic外部對象“。如果無需訪問外部數據,這組方括號可以為空。Static對象,諸如cout,是可被使用的。
在lambda introducer 和lambda body之間,你可以指明參數或mutable,或一份異常明細(exception specification)或attribute specifier以及返回類型。所有這一切都可有可無,但如果其中一個出現了,參數所需的小括號就必須出現。 因此lambda語法也可以是:
[…] {…}
或
[…] (…)
Lambda 可以擁有參數,指明于小括號內,就像任何函數一樣:
auto l=[](const std::string& s){std::cout<<s<<std::endl; }; l("hello lambda");然而,請注意,lambda不可以是template,你始終必須指明所有類型。
lambda也可以返回某物。但你不需要指明返回類型,該類型會根據返回值被推導出來。例如下面的lambda的返回類型是int:
[]{return 42; }如果一定想指明一個返回類型,可使用新式C++語法
[]() -> double{return 42; }會返回42.0
這里指明返回類型,放在實參所需要的小括號以及 字符->以后
下面是一個lambda表達式的使用用例:
auto num=[](const int & n){return n;};auto n=num(2);cout<<n<<endl;輸出結果:2
Capture(用以訪問外部作用域)
在lambda introducer(每個lambda最開始的方括號)內,你可以指明一個capture用來處理外部作用域內未被傳遞為實參的數據:
- [=]意味著外部作用域以by value方式傳遞給lambda。因此當這個lambda被定義時,你可以讀取所有可讀數據,但不能改動它們。
- [&]意味著外部作用域以 by reference 方式傳遞給lambda。因此當這個lambda被定義時,你對所有數據的涂寫動作都是合法的,前提是你擁有涂寫它們的權力。
也可以個別指明lambda之內你所訪問的每一個對象是by value或 by reference。因此你可以對訪問設限,也可以混合不同的訪問權力。例如下面這些語句:
int x=0; int y=42; auto qqq=[x,&y]{cout<<"x: "<<x<<endl;cout<<"y: "<<y<<endl;++y;} x=y=77;qqq(); qqq();cout<<"final y: "<<y<<endl;輸出結果
你也可以寫[=,&y] 取代[x,&y],意思是以by reference 方式傳遞y,其他所有實參則以by value 方式傳遞。
為了獲得passing by value 和passing by reference 混合體,你可以聲明lambda為mutable。下例中的對象都以by value 方式傳遞,但在這個lambda 所定義的函數對象內,你有權利涂寫傳入的值。
例如
int id=0; auto f=[id] () mutable{cout<<"id: "<<id<<endl;++id;}; id =42; f(); f(); f(); cout<<id<<endl;輸出結果
可以把上述lambda的行為視同下面這個function object
class{private:int id;// copy of outside idpublic:void operator(){cout<<"id: "<<id<<endl;++id;} };由于mutable的緣故,operator()被定義為一個non-const成員函數,那意味著對id的涂寫是可能的。 所以,有了mutable,lambda變得stateful,即使state是以 by value方式傳遞。 如果沒有指明mutable(一般往往如此),operator()就成為一個const成員函數,那么對于對象你就只能讀取,因為它們都是以值傳遞的。
總結
以上是生活随笔為你收集整理的C++11语言新特性-《C++标准库(第二版)》读书笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 算法刷题-数论-质数的判定、分解质因数、
- 下一篇: Leetcode1711. 大餐计数[C