C++11 帶來的優雅語法

大CC發表於2016-03-26

自動型別推導 auto

auto的自動型別推導,用於從初始化表示式中推斷出變數的資料型別。通過auto的自動型別推導,可以簡化我們的程式設計工作;

auto是在編譯時對變數進行了型別推導,所以不會對程式的執行效率造成不良影響;

另外,似乎auto也並不會影響編譯速度,因為編譯時本來也要右側推導然後判斷與左側是否匹配。

auto a; // 錯誤,auto是通過初始化表示式進⾏型別推導,如果沒有初始化表示式,就無法確定a的型別

 auto i = 1;
 auto d = 1.0;
 auto str = "Hello World"
 auto ch = 'A'

auto對引用的推導預設為值型別,可以指定引用修飾符設定為引用:

int x = 5;
 int & y = x;
 auot z = y ;// z 為int
 auto & z = y; // z的型別為 int&

對指標的推導預設為指標型別,當然,也可以指定*修飾符(效果一樣):

int  *px = &x;
auto py = px;
auto*py = px;

推導常量

const int *px = &x;
 auto py = px; //py的型別為 const int *
 const auto py = px ; //py的型別為const int *

萃取型別 decltype

decltype實際上有點像auto的反函式,使用auto可以用來宣告一個指定型別的變數,而decltype可以通過一個變數(或表示式)得到型別;

#include
int main() {
 int x = 5;
 decltype(x) y = x; //等於 auto y = x;
 const std::vector v(1);
 auto a = v[0]; // a has type int
 decltype(v[1]) b = 1; // b has type const int&, the return type of
 // std::vector::operator[](size_type) const
 auto c = 0; // c has type int
 auto d = c; // d has type int
 decltype(c) e; // e has type int, the type of the entity named by c
 decltype((c)) f = c; // f has type int&, because (c) is an lvalue
 decltype(0) g; // g has type int, because 0 is an rvalue
}

有沒有聯想到STL中的萃取器?寫模版時有了這個是不是會方便很多;

返回型別後置語法 Trailing return type

C++11支援返回值後置

例如:

int adding_func(int lhs, int rhs);

可以寫為:

auto adding_func(int lhs, int rhs) -> int

auto用於佔位符,真正的返回值在後面定義;

這樣的語法用於在編譯時返回型別還不確定的場合;

比如有模版的場合中,兩個型別相加的最終型別只有執行時才能確定:

template
auto adding_func(const Lhs &lhs, const Rhs &rhs) -> decltype(lhs+rhs) 
{return lhs + rhs;}
cout << adding_func<double,int>(dv,iv) << endl;

auto用於佔位符,真正的返回值型別在程式執行中,函式返回時才確定;

不用auto佔位符,直接使用decltype推導型別:

decltype(lhs+rhs) adding_func(const Lhs &lhs, const Rhs &rhs)

這樣寫,編譯器無法通過,因為模版引數lhs和rhs在編譯期間還未宣告;

當然,這樣寫可以編譯通過:

decltype( (*(Lhs*)0) + (*(Rhs*)0) ) adding_func(const Lhs &lhs, const Rhs &rhs)

但這種形式實在是不直觀,不如auto佔位符方式直觀易懂;

空指標標識 nullptr

空指標標識(nullptr)(其本質是一個內建的常量)是一個表示空指標的標識,它不是一個整數。這裡應該與我們常用的NULL巨集相區別,雖然它們都是用來表示空置針,但NULL只是一個定義為常整數0的巨集,而nullptr是C++11的一個關鍵字,一個內建的識別符號。

nullptr和任何指標型別以及類成員指標型別的空值之間可以發生隱式型別轉換,同樣也可以隱式轉換為bool型(取值為false)。但是不存在到整形的隱式型別轉換。

有了nullptr,可以解決原來C++中NULL的二義性問題;

voidF(int a){
    cout<<a<<endl;
}
voidF(int*p){
    assert(p != NULL);
    cout<< p <<endl;
}

int main(){
    int*p = nullptr;
    int*q = NULL;
    bool equal = ( p == q ); // equal的值為true,說明p和q都是空指標
    int a = nullptr; // 編譯失敗,nullptr不能轉型為int
    F(0); // 在C++98中編譯失敗,有二義性;在C++11中呼叫F(int)
    F(nullptr);

    return 0;
}

區間迭代 range-based for loop

C++11擴充套件了for的語法,終於支援區間迭代,可以便捷的迭代一個容器的內的元素;

int my_array[5] = {1, 2, 3, 4, 5};
// double the value of each element in my_array:
for (int &x : my_array) {
    x *= 2;
}

當然,這時候使用auto會更簡單;

for (auto &x : my_array) {
    x *= 2;
}

如果有更為複雜的場景,使用auto的優勢立刻體現出來:

map<string,int> map;
map.insert<make_pair<>("ss",1);
for(auto &x : my_map)
{
   cout << x.first << "/" << x.second;
}

去除右尖括號的蹩腳語法 right angle brackets

在C++98標準中,如果寫一個含有其他模板型別的模板:

vector<vector > vector_of_int_vectors;

你必須在結束的兩個’>‘之間新增空格。這不僅煩人,而且當你寫成>>而沒有空格時,你將得到困惑和誤導的編譯錯誤資訊。產生這種行為的原因是C++詞法分析的最大匹配原則(maximal munch rule)。一個好訊息是從今往後,你再也不用擔心了:

vector<vector> vector_of_int_vectors;

在C++98中,這是一個語法錯誤,因為兩個右角括號(‘>’)之間沒有空格(譯註:因此,編譯器會將它分析為”>>”操作符)。C++0x可以正確地分辨出這是兩個右角括號(‘>’),是兩個模板引數列表的結尾。

為什麼之前這會是一個問題呢?一般地,一個編譯器前端會按照“分析/階段”模型進行組織。簡要描述如下:

詞法分析(從字元中構造token)

語法分析(檢查語法)

型別檢查(確定名稱和表示式的型別)

這些階段在理論上,甚至在某些實際應用中,都是嚴格獨立的。所以,詞法分析器會認為”>>”是一個完整的token(通常意味著右移操作符或是輸入),而無法理解它的實際意義(譯註:即在具體的上下文環境下,某一個符號的具體意義)。特別地,它無法理解模板或內建模板引數列表。然而,為了使上述示例“正確”,這三個階段必須進行某種形式的互動、配合。解決這個問題的最關鍵的點在於,每一個C++ 編譯器已完整理解整個問題(譯註:對整個問題進行了全部的詞法分析、符號分析及型別檢測,然後分析各個階段的正確性),從而給出令人滿意的錯誤訊息。

lambda表示式的引入

對於為標準庫演算法寫函式/函式物件(function object)這個事兒大家已經抱怨很久了(例如Cmp)。特別是在C++98標準中,這會令人更加痛苦,因為無法定義一個區域性的函式物件。

首先,我們需要在我們實現的邏輯作用域(一般是函式或類)外部定義比較用的函式或函式物件,然後,才能使用:

bool myfunction (int i,int j) { return (i<j); }

struct myclass {
  bool operator() (int i,int j) { return (i<j);}
} myobject;

int main()
{
    int myints[] = {32,71,12,45,26,80,53,33};
    std::vector myvector (myints, myints+8);
     // using function as comp
    std::sort (myvector.begin(), myvector.end(), myfunction); 
      // using function object as comp
      std::sort (myvector.begin(), myvector.end(), myobject);
}

不過現在好多了,lambda表示式允許用”inline”的方式來寫函式了:

sort(myvector.begin(), myvector.end(), [](int i, int j) { return i< j; });

真是親切!lambda的引入應該會增加大家對STL演算法的使用頻率;

原生字串 Raw string literals

比如,你用標準regex庫來寫一個正規表示式,但正規表示式中的反斜槓’’其實卻是一個“轉義(escape)”操作符(用於特殊字元),這相當令人討厭。考慮如何去寫“由反斜槓隔開的兩個詞語”這樣一個模式(w\w):

string s = "\\w\\\\\\w";  // 不直觀、且容易出錯

請注意,在正規表示式和普通C++字串中,各自都需要使用連續兩個反斜槓來表示反斜槓本身。然而,假如使用C++11的原生字串,反斜槓本身僅需一個反斜槓就可以表示。因而,上述的例子簡化為:

string s = R"(\w\\\w)";  // ok

非成員begin()和end()

非成員begin()和end()函式。他們是新加入標準庫的,除了能提高了程式碼一致性,還有助於更多地使用泛型程式設計。它們和所有的STL容器相容。更重要的是,他們是可過載的。所以它們可以被擴充套件到支援任何型別。對C型別陣列的過載已經包含在標準庫中了。

在這個例子中我列印了一個陣列然後查詢它的第一個偶數元素。如果std::vector被替換成C型別陣列。程式碼可能看起來是這樣的:

int arr[] = {1,2,3};
std::for_each(&arr[0], &arr[0]+sizeof(arr)/sizeof(arr[0]), [](int n) {std::cout << n << std::endl;});

auto is_odd = [](int n) {return n%2==1;};
auto begin = &arr[0];
auto end = &arr[0]+sizeof(arr)/sizeof(arr[0]);
auto pos = std::find_if(begin, end, is_odd);
if(pos != end)
std::cout << *pos << std::endl;

如果使用非成員的begin()和end()來實現,就會是以下這樣的:

int arr[] = {1,2,3};
std::for_each(std::begin(arr), std::end(arr), [](int n) {std::cout << n << std::endl;});

auto is_odd = [](int n) {return n%2==1;};
auto pos = std::find_if(std::begin(arr), std::end(arr), is_odd);
if(pos != std::end(arr))
std::cout << *pos << std::endl;

這基本上和使用std::vecto的程式碼是完全一樣的。這就意味著我們可以寫一個泛型函式處理所有支援begin()和end()的型別。

初始化列表及統一初始化方法 Initializer lists

在C++98中,對vector的多個初始化,我們需要這樣:

int myints[] = { 10, 20, 30, 30, 20, 10, 10, 20 };
std::vector myvector (myints, myints+8);

現在,我們可以這樣:

std::vector second ={10, 20, 30, 30, 20, 10, 10, 20};

初始化表有時可以像引數那樣方便的使用。看下邊這個例子(x,y,z是string變數,Nocase是一個大小寫不敏感的比較函式):

auto x = max({x,y,z},Nocase());

初始化列表不再僅限於陣列。對於常見的map、string等,我們可以使用以下語法來進行初始化:

int arr[3]{1, 2, 3};
vector iv{1, 2, 3};
map<int, string>  m{{1, "a"}, {2, "b"}};
string str{"Hello World"};

可以接受一個“{}列表”對變數進行初始化的機制實際上是通過一個可以接受引數型別為std::initializer_list的函式(通常為建構函式)來實現的。例如:

void f(initializer_list);
f({1,2});
f({23,345,4567,56789});
f({});  // 以空列表為引數呼叫f()
f{1,2}; // 錯誤:缺少函式呼叫符號( )
years.insert({{"Bjarne","Stroustrup"},{1950, 1975, 1985}});

初始化列表可以是任意長度,但必須是同質的(所有的元素必須屬於某一模板型別T, 或可轉化至T型別的)。

容器可以用如下方式來實現“初始化列表建構函式”:

template class vector {
 public:
 // 初始化列表建構函式
 vector (std::initializer_list s)
 {
 // 預留出合適的容量
 reserve(s.size()); //
 // 初始化所有元素
 uninitialized_copy(s.begin(), s.end(), elem);
 sz = s.size(); // 設定容器的size
 }
 // ... 其他部分保持不變 ...
};

使用“{}初始化”時,直接構造與拷貝構造之間仍有細微差異,但不再像以前那樣明顯。例如,std::vector擁有一個引數型別為int的顯式建構函式及一個帶有初始化列表的建構函式:

vector v1(7); // OK: v1有7個元素
    v1 = 9; // Err: 無法將int轉換為vector
vector v2 = 9; // Err: 無法將int轉換為vector
void f(const vector&);
    f(9); // Err: 無法將int轉換為vector
vector v1{7}; // OK: v1有一個元素,其值為7.0
    v1 = {9}; // OK: v1有一個元素,其值為9.0
vector v2 = {9}; // OK: v2有一個元素,其值為9.0
    f({9}); // OK: f函式將以列表{9}為引數被呼叫
vector<vector> vs = {
   vector(10), // OK, 顯式構造(10個元素,都是預設值0.0)
   vector{10}, // OK:顯式構造(1個元素,值為10.0)
   10 // Err :vector的建構函式是顯式的
};

函式可以將initializer_list作為一個不可變的序列進行讀取。例如:

void f(initializer_list args)
{
    for (auto p=args.begin(); p!=args.end(); ++p)
        cout << *p << "\n";
}

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

標準庫容器,string型別及正規表示式均具有初始化列表建構函式,以及(初始化列表)賦值函式等。初始化列表亦可作為一種“序列”以供“序列化for語句”使用。

參考

http://www.stroustrup.com/C++11FAQ.html

https://www.chenlq.net/books/cpp11-faq

相關文章