C++11帶來的優雅語法


C++11帶來的優雅語法

自動類型推導 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 <vector>
int main() {
    int x = 5;
    decltype(x) y = x; //等於 auto y = x;
    const std::vector<int> 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<int>::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<class Lhs, class Rhs>
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<int> > vector_of_int_vectors;

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

vector<vector<int>> 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<int> 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<int> myvector (myints, myints+8); 

現在,我們可以這樣:

std::vector<int> 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<int> iv{1, 2, 3};
map<int, string>  m{{1, "a"}, {2, "b"}};
string str{"Hello World"};

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

void f(initializer_list<int>);
f({1,2});
f({23,345,4567,56789});
f({});  // 以空列表為參數調用f()
f{1,2}; // 錯誤:缺少函數調用符號( )
years.insert({{"Bjarne","Stroustrup"},{1950, 1975, 1985}});

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

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

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

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

vector<double> v1(7); // OK: v1有7個元素<br />
v1 = 9;         // Err: 無法將int轉換為vector
vector<double> v2 = 9;    // Err: 無法將int轉換為vector

void f(const vector<double>&);
f(9);               // Err: 無法將int轉換為vector

vector<double> v1{7};     // OK: v1有一個元素,其值為7.0
v1 = {9};           // OK: v1有一個元素,其值為9.0
vector<double> v2 = {9};  // OK: v2有一個元素,其值為9.0
f({9});  // OK: f函數將以列表{9}為參數被調用

vector<vector<double>> vs = {
    vector<double>(10), // OK, 顯式構造(10個元素,都是默認值0.0)
    vector<double>{10}, // OK:顯式構造(1個元素,值為10.0)
        10          // Err :vector的構造函數是顯式的
};

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

void f(initializer_list<int> 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

Posted by: 大CC | 11SEP,2015
博客:blog.me115.com [訂閱]
Github:大CC


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM