C++統一初始化語法(列表初始化)

引言

要是世上未曾存在C++14和C++17該有多好!constexpr是好東西,可是讓編譯器開發者痛不欲生;新標準庫的確好用,但改語法細節未必是明智之舉,尤爲是3年一次的頻繁改動。C++帶了太多歷史包袱,咱們都是爲之買帳的一員。html

我沒那麼多精力考慮C++14/17的問題,因此本文基於C++11標準。ios

知其因此然,是學習C++愈加複雜的語法的最佳方式。所以,咱們從列表初始化的動機講起。數組

 

動機

早在2005年,Bjarne Stroustrup就提出要統一C++中的初始化語法。這是由於在C++11之前,初始化存在一系列問題,包括:函數

  • 4種初始化方式:X t1 = v;X t2(v);X t3 = { v };X t4 = X(v);工具

  • 聚合(aggregate)初始化;學習

  • defaultexplicit測試

  • ……代理

雖然每個都有辦法解決,但加在一塊兒將會變得很是複雜,對編譯器和開發者都是負擔。換句話說,惟一的需求就是一種統一的初始化語法,其適用範圍能涵蓋先前的各類問題。code

因而,列表初始化誕生了。htm

 

語法

正由於列表初始化是爲解決初始化問題而生,列表初始化的適用範圍是任何初始化。你能想到的都寫寫看,寫對就是賺到。

固然,全憑感受是行不通的,仍是得講點道理。列表初始化分爲兩類:直接初始化與拷貝初始化。

在直接初始化中,不管構造函數是否explicit,都有可能被調用:

  1. T object { arg1, arg2, ... };,用arg1, arg2, ...構造T類型的對象object——參數能夠是一個值,也能夠是一個初始化列表,下同;

  2. Class { T member { arg1, arg2, ... }; };,構造member成員對象——花括號的優點在這裏體現出來,由於若是是圓括號的話member會被看做一個函數;

  3. T { arg1, arg2, ... },構造臨時對象;

  4. new T { arg1, arg2, ... },構造heap上的對象;

  5. Class::Class() : member{arg1, arg2, ...} {...,成員初始化列表——除了2之外,其他都與用()初始化沒有區別。

在拷貝初始化中,不管構造函數是否explicit都會被考慮,可是若是重載決議爲一個explicit函數,則此調用錯誤:

  1. T object = {arg1, arg2, ...};,與直接初始化中的1相似,除了explicit之外都相同,operator=不會被調用;

  2. object = { arg1, arg2, ... },賦值語句,調用operator=

  3. Class { T member = { arg1, arg2, ... }; };,與直接初始化中的2相似,explicit同理;

  4. function( { arg1, arg2, ... } ),構造函數參數;

  5. return { arg1, arg2, ... } ;,構造返回值;

  6. object[ { arg1, arg2, ... } ],構造operator[]的參數;

  7. U( { arg1, arg2, ... } ),構造U構造函數的參數。

4~7能夠歸納爲,在該有一個對象的地方,能夠用一個列表來構造它。這句話不是很嚴謹,由於除了operator()operator[]之外,其餘運算符的參數都不能用列表初始化。

還有一個要注意的地方,是列表初始化不容許窄化轉換(narrowing conversion),便可能丟失信息的轉換,如float轉換爲int

#include <iostream>
#include <utility>

struct Test
{
    Test(int, int)
    {
        std::cout << "Test(int, int)" << std::endl;
    }
    explicit Test(int, int, int)
    {
        std::cout << "explicit Test(int, int, int)" << std::endl;
    }
    void operator[](std::pair<int, int>)
    {
        std::cout << "void operator[](std::pair<int, int>)" << std::endl;
    }
    void operator()(std::pair<int, int>)
    {
        std::cout << "void operator()(std::pair<int, int>)" << std::endl;
    }
};

Test test()
{
    return { 1, 2 };
}

int main()
{
    Test t{ 1, 2 };
    Test t1 = { 1, 2 };
    Test t2 = { 1, 2, 3 }; // error
    t[{ 1, 2 }];
    t({ 1, 2 });
}

 

initializer_list

列表不是表達式,更不屬於任何類型,因此decltype({1, 2})是非法的,這還適用於模板參數推導。可是在如下幾種狀況中,列表能夠轉換成std::initializer_list<T>實例:

  1. 直接初始化中,對應構造函數參數類型爲std::initializer_list<T>

  2. 拷貝初始化中,對應參數類型爲std::initializer_list<T>

  3. 綁定到auto上(列表元素類型必須嚴格一致),包括範圍for(range for)循環——當綁定auto&&時,變量的實際類型爲std::initializer_list<T>&&,這是轉發引用的特例。

std::initializer_list是爲列表初始化提供的特殊的工具,是一個輕量級的數組代理(proxy),其元素類型爲const T。雖然你能在<initializer_list>中看到std::initializer_list類模板的實現,但它其實是與編譯器內部綁定的,你沒法用一個本身寫的類似的類替換它(除非改編譯器)。

std::initializer_list有構造函數、sizebeginend函數,用法與其餘STL順序容器相似。迭代器解引用獲得const T&類型,元素是不能修改的。

std::initializer_list帶來的最明顯的進步就是STL容器能夠用列表來初始化,無需再寫那麼多push_back了。

 

重載決議

struct Test
{
    Test(int, int)
    {
        std::cout << "Test(int, int)" << std::endl;
    }
    Test(std::initializer_list<int>)
    {
        std::cout << "Test(std::initializer_list<int>)" << std::endl;
    }
};

若是我寫Test{1, 2},哪一個構造函數會被調用呢?回答這個問題,須要對與列表相關的重載決議有所瞭解。

對於涉及到構造函數的列表初始化(不涉及到的包括聚合初始化等),各構造函數分兩個階段考慮:

  1. 若是有構造函數第一個參數爲std::initializer_list,沒有其餘參數或其餘參數都有默認值,則匹配該構造函數(這裏彷佛容許窄化轉換,我測試起來也是如此)——std::initializer_list優先級高

  2. 不然,全部構造函數參與重載決議,除了窄化轉換不容許,以及拷貝初始化與explicit的衝突依然有效。

因此上面那段程序中Test{1, 2}會匹配第二個構造函數。

若是有多個std::initializer_list重載呢?衆所周知,重載決議中參數轉換有完美、提高、轉換三個等級,std::initializer_list參數的轉換等級定義爲全部元素中最差的(不容許窄化轉換),而後找出等級最高的調用,若是有多個則爲二義調用。

若是沒有std::initializer_list重載呢?因爲從列表到參數自己就是轉換,屬於最差的等級,若是有多個函數能夠經過參數轉換後匹配,則該調用就是二義調用;只有當只有一個函數可行時才合法。

 

總結

列表初始化是一種萬能的初始化語法,適用範圍廣致使其規則比較複雜,咱們應當結合其動機來理解標準規定的行爲。

列表初始化包括直接初始化與拷貝初始化,後者涵蓋了參數與返回值等情形。當咱們不想要隱式拷貝初始化時,要用explicit關鍵字來拒絕。

列表不屬於任何類型,但一些狀況下能夠轉換成std::initializer_list。在重載決議中,std::initializer_list有更高的優先級。

相關文章
相關標籤/搜索