【C++】C++11統一初始化(initializer_list源碼分析)

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與重載構造函數的關係:

  1. 當構造函數形參中不帶initializer_list時,小括號和大括號的意義沒有區別;
  2. 如果構造函數中帶有initializer_list形參,採用大括號初始化語法會強烈優先匹配帶有initializer_list形參的重載版本,而其他更精確匹配的版本可能沒有機會被匹配
  3. 空大括號構造一個對象時,表示沒有參數(而不是空的initializer_list對象),因此,會匹配默認的無參構造函數,而不是匹配initializer_list形參的版本的構造函數;
  4. 拷貝構造函數和移動構造函數也可能被帶有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構造函數。

除此之外,還有如下幾個注意點:

  1. initializer_list是一個輕量級的容器類型,內部定義了iterator等容器必需的概念。其中有3個成員接口:size()、begin()和end()。遍歷時取得的迭代器是隻讀的,無法修改其中的某一個元素的值;
  2. 對於initializer_list而言,它可以接收任意長度的初始化列表,但要求元素必須是同種類型T(或可轉換爲T)
  3. 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()); }

相關閱讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章