引言
要是世上不曾存在C++14和C++17該有多好!constexpr是好東西,但是讓編譯器開發者痛不欲生;新標準庫的確好用,但改語法細節未必是明智之舉,尤其是3年一次的頻繁改動,C++帶了太多歷史包袱,我們都是為之買賬的一員,
我沒那么多精力考慮C++14/17的問題,所以本文基于C++11標準,
知其所以然,是學習C++越發復雜的語法的最佳方式,因此,我們從串列初始化的動機講起,
動機
早在2005年,Bjarne Stroustrup就提出要統一C++中的初始化語法,這是因為在C++11以前,初始化存在一系列問題,包括:
-
4種初始化方式:
X t1 = v;、X t2(v);、X t3 = { v };、X t4 = X(v);; -
聚合(aggregate)初始化;
-
default與explicit; -
……
雖然每一個都有辦法解決,但加在一起將會變得非常復雜,對編譯器和開發者都是負擔,換句話說,唯一的需求就是一種統一的初始化語法,其適用范圍能涵蓋先前的各種問題,
于是,串列初始化誕生了,
語法
正因為串列初始化是為解決初始化問題而生,串列初始化的適用范圍是任何初始化,你能想到的都寫寫看,寫對就是賺到,
當然,全憑感覺是行不通的,還是得講點道理,串列初始化分為兩類:直接初始化與拷貝初始化,
在直接初始化中,無論建構式是否explicit,都有可能被呼叫:
-
T object { arg1, arg2, ... };,用arg1, arg2, ...構造T型別的物件object——引數可以是一個值,也可以是一個初始化串列,下同; -
Class { T member { arg1, arg2, ... }; };,構造member成員物件——花括號的優勢在這里體現出來,因為如果是圓括號的話member會被看作一個函式; -
T { arg1, arg2, ... },構造臨時物件; -
new T { arg1, arg2, ... },構造heap上的物件; -
Class::Class() : member{arg1, arg2, ...} {...,成員初始化串列——除了2以外,其余都與用()初始化沒有區別,
在拷貝初始化中,無論建構式是否explicit都會被考慮,但是如果多載決議為一個explicit函式,則此呼叫錯誤:
-
T object = {arg1, arg2, ...};,與直接初始化中的1類似,除了explicit以外都相同,operator=不會被呼叫; -
object = { arg1, arg2, ... },賦值陳述句,呼叫operator=; -
Class { T member = { arg1, arg2, ... }; };,與直接初始化中的2類似,explicit同理; -
function( { arg1, arg2, ... } ),建構式引數; -
return { arg1, arg2, ... } ;,構造回傳值; -
object[ { arg1, arg2, ... } ],構造operator[]的引數; -
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>實體:
-
直接初始化中,對應建構式引數型別為
std::initializer_list<T>; -
拷貝初始化中,對應引數型別為
std::initializer_list<T>; -
系結到
auto上(串列元素型別必須嚴格一致),包括范圍for(range for)回圈——當系結auto&&時,變數的實際型別為std::initializer_list<T>&&,這是轉發參考的特例,
std::initializer_list是為串列初始化提供的特殊的工具,是一個輕量級的陣列代理(proxy),其元素型別為const T,雖然你能在<initializer_list>中看到std::initializer_list類模板的實作,但它實際上是與編譯器內部系結的,你無法用一個自己寫的相似的類替換它(除非改編譯器),
std::initializer_list有建構式、size、begin和end函式,用法與其他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},哪個建構式會被呼叫呢?回答這個問題,需要對與串列相關的多載決議有所了解,
對于涉及到建構式的串列初始化(不涉及到的包括聚合初始化等),各建構式分兩個階段考慮:
-
如果有建構式第一個引數為
std::initializer_list,沒有其他引數或其他引數都有默認值,則匹配該建構式(這里似乎允許窄化轉換,我測驗起來也是如此)——std::initializer_list優先級高; -
否則,所有建構式參與多載決議,除了窄化轉換不允許,以及拷貝初始化與
explicit的沖突依然有效,
所以上面那段程式中Test{1, 2}會匹配第二個建構式,
如果有多個std::initializer_list多載呢?眾所周知,多載決議中引數轉換有完美、提升、轉換三個等級,std::initializer_list引數的轉換等級定義為所有元素中最差的(不允許窄化轉換),然后找出等級最高的呼叫,如果有多個則為二義呼叫,
如果沒有std::initializer_list多載呢?由于從串列到引數本身就是轉換,屬于最差的等級,如果有多個函式可以通過引數轉換后匹配,則該呼叫就是二義呼叫;只有當只有一個函式可行時才合法,
總結
串列初始化是一種萬能的初始化語法,適用范圍廣導致其規則比較復雜,我們應當結合其動機來理解標準規定的行為,
串列初始化包括直接初始化與拷貝初始化,后者涵蓋了引數與回傳值等情形,當我們不想要隱式拷貝初始化時,要用explicit關鍵字來拒絕,
串列不屬于任何型別,但一些情況下可以轉換成std::initializer_list,在多載決議中,std::initializer_list有更高的優先級,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/30637.html
標籤:C++
上一篇:五大分布式事務,你了解多少?
