C++11之前的初始化語法很亂,有四種初始化方式,而且每種之前甚至不能相互轉換。讓人有種剪不斷,理還亂的感覺。因此,C++11添加了統一初始化的方式,本文將對統一初始化的語法進行詳細講解。
本文實例源碼github地址:https://github.com/yngzMiao/yngzmiao-blogs/tree/master/2020Q2/20200404。
統一初始化
幾種初始化方式
先來看一下,C++用於的幾種初始化的方式,以int爲例:
小括號:int x(0); //C++98
等號:int x = 0; //C++98
大括號:int x{0}; //C++98成功,C++11成功
等號和大括號:int x = {0}; //C++98失敗,C++11成功
可以看出,C++擁有較多的初始化方式,如此便引申出一種統一初始化的方式。
統一初始化方式
統一初始化
,也叫做大括號初始化
。顧名思義,是使用大括號進行初始化的方式。例如:
#include <iostream>
#include <vector>
#include <complex>
int main(int argc, char *argv[]) {
int values[]{1, 2, 3};
std::vector<int> v{2, 3, 5, 7, 11, 13, 17};
std::vector<std::string> cities{
"Beijing", "Nanjing", "Shanghai", "Hangzhou"
};
std::complex<double> c{3.0, 4.0};
return 0;
}
其實是利用一個事實:編譯器看到{t1, t2, …, tn}便會做出一個initializer_list,它關聯到一個array<T, n>。調用構造函數的時候,該array內的元素會被編譯器分解逐一傳給函數。但若函數的參數就是initializer_list,則不會逐一分解,而是直接調用該參數的函數。
例如:vector類型的cities,由於採用{}進行初始化,會形成一個initializer_list,它會關聯到一個array<string, 4>。調用vector構造函數時,發現正好有一個接收initializer_list的參數,於是直接調用構造函數。但是complex類型的c,就沒有以initializer_list爲參數的構造函數,於是在初始化的時候,只能從array中將元素逐一傳遞給構造函數進行初始化。
所有的標準容器的構造函數都有以initializer_list爲參數的構造函數。
initializer_list
簡單用法
使用initizlizer_list
的最廣泛的使用就是:不定長度同類型參數的情況。
#include <iostream>
void print(std::initializer_list<int> vals) {
for(auto iter = vals.begin(); iter != vals.end(); ++iter)
std::cout << *iter << std::endl;
}
int main(int argc, char *argv[]) {
print({1, 2, 3, 4, 5});
return 0;
}
對於initizlizer_list,可以利用iterator
來對包含的元素進行遍歷來完成所需的一些操作。
構造函數的匹配
當initizlizer_list做參數與其他參數的函數形成重載關係的時候,如何進行函數的選擇呢?
#include <iostream>
class P {
public:
P() {
std::cout << "P" << std::endl;
}
P(int a, int b) {
std::cout << "P(int, int) " << a << " " << b << std::endl;
}
P(std::initializer_list<int> vars) {
std::cout << "P(initializer_list) ";
for(auto a : vars)
std::cout << a << " ";
std::cout << std::endl;
}
};
int main(int argc, char *argv[]) {
P p1(1, 2); // P(int, int) 1 2
P p2{1, 2}; // P(initializer_list) 1 2
P p3{1, 2, 3}; // P(initializer_list) 1 2 3
P p4 = {1, 2}; // P(initializer_list) 1 2
P p5{}; // P
return 0;
}
在這個例子中,p1使用小括號進行初始化,直接調用第一個構造函數,這是沒有什麼可猶豫的。p2、p3、p4都使用的大括號進行初始化,會形成一個initializer_list,而且正好存在以該類型爲參數的構造函數,直接調用該構造函數直接進行初始化。p5儘管使用的是大括號進行初始化,但使用的是空大括號,表示沒有參數,會調用無參構造函數。
如果,P類中僅僅只有第一個構造函數,那麼情形又會變成什麼呢?
#include <iostream>
class P {
public:
P(int a, int b) {
std::cout << "P(int, int) " << a << " " << b << std::endl;
}
};
int main(int argc, char *argv[]) {
P p1(1, 2); // P(int, int) 1 2
P p2{1, 2}; // P(int, int) 1 2
P p3{1, 2, 3}; // Error 出錯
P p4 = {1, 2}; // P(int, int) 1 2
return 0;
}
此時由於沒有以initializer_list類型爲參數的構造函數,p2、p4內的元素會將會被拆解,逐一傳遞給構造函數進行初始化。但是,p3由於元素的個數爲3個,與構造函數的參數數量不同,不可以調用。
總結下,initializer_list與重載構造函數的關係:
- 當構造函數形參中不帶initializer_list時,小括號和大括號的意義沒有區別;
- 如果構造函數中帶有initializer_list形參,採用大括號初始化語法會強烈優先匹配帶有initializer_list形參的重載版本,而其他更精確匹配的版本可能沒有機會被匹配;
- 空大括號構造一個對象時,表示沒有參數(而不是空的initializer_list對象),因此,會匹配默認的無參構造函數,而不是匹配initializer_list形參的版本的構造函數;
- 拷貝構造函數和移動構造函數也可能被帶有initializer_list形參的構造函數劫持。
源碼分析
下面通過對initizlizer_list的源碼,分析來探究其深層次的原理:
template<class _E>
class initializer_list
{
public:
typedef _E value_type;
typedef const _E& reference;
typedef const _E& const_reference;
typedef size_t size_type;
typedef const _E* iterator;
typedef const _E* const_iterator;
private:
iterator _M_array;
size_type _M_len;
// The compiler can call a private constructor.
constexpr initializer_list(const_iterator __a, size_type __l)
: _M_array(__a), _M_len(__l) { }
public:
constexpr initializer_list() noexcept
: _M_array(0), _M_len(0) { }
constexpr size_type size() const noexcept { return _M_len; }
constexpr const_iterator begin() const noexcept { return _M_array; }
constexpr const_iterator end() const noexcept { return begin() + size(); }
};
可以看到initializer_list內部存儲了兩個變量:_M_array(迭代器變量)和_M_len(長度)。當調用構造函數的時候,就會將這兩個變量進行初始化賦值。那這兩個變量是怎麼來的呢?
其實,當用{}進行初始化的時候,首先會創建一個array,並將初始化元素存放起來。然後,調用initializer_list的構造函數,用array首元素的迭代器和array的元素個數,進行初始化。
如果仔細看會發現,initializer_list構造函數是private類型的,按道理來說,是沒有辦法外部調用的!但是,在源碼中也註明了,編譯器可以調用該private構造函數。
除此之外,還有如下幾個注意點:
- initializer_list是一個輕量級的容器類型,內部定義了iterator等容器必需的概念。其中有3個成員接口:size()、begin()和end()。遍歷時取得的迭代器是隻讀的,無法修改其中的某一個元素的值;
- 對於initializer_list而言,它可以接收任意長度的初始化列表,但要求元素必須是同種類型T(或可轉換爲T);
- Initializer_list內部並不負責保存初始化列表中的元素拷貝,僅僅是列表中元素的引用而己。因此,通過過拷貝構造對象與原對象共享列表中的元素空間。也就是說,initializer_list的內部並沒有內含該array的內容,僅僅是擁有指向array的迭代器。如果拷貝構造或者拷貝賦值的話,array的內容只有一份,但有兩份迭代器指向。如果對initializer_list對象copy一個副本,默認是淺拷貝,此時兩個對象指向同一個array。這是危險的。
也就是說,下面的情形是不允許的:
std::initializer_list<int> func(void)
{
int a = 1, b = 2;
return {a, b}; //由於initializer_list保存的是對象的引用,但a與b是局部變量在
//func返回後會被釋放,initializer_list內部會存在空懸指針!危險!
//正確的做法可以將返回值改爲保存副本的容器,如vector<int>
}
//注意下面s1、 s2、s3和s4均共享元素空間
initializer_list<string> s1 = { "aa", "bb", "cc", "dd" };
initializer_list<string> s2 = s1;
initializer_list<string> s3(s1);
initializer_list<string> s4;
s4 = s1;
其他
之前版本的C++,min、max只可以進行兩個數之間的比較,但是有了initializer_list之後,現在支持如下的比較:
std::max({4, 3, 5, 7});
std::min({4, 3, 5, 7});
這是因爲,源碼中增加了如下的定義:
template<typename _Tp>
inline _Tp max(initializer_list<_Tp> __l)
{ return *std::max_element(__l.begin(), __l.end()); }
template<typename _Tp>
inline _Tp min(initializer_list<_Tp> __l)
{ return *std::min_element(__l.begin(), __l.end()); }