C++ 2.0新特性

tlfclwx發表於2021-05-05

C++ standard之演化

  • C++ 98(1.0)
  • C++ 03(TR1, technical Report 1) // 一個實驗性的版本
  • C++ 11(2.0)
  • C++ 14

此次記錄涵蓋了C++ 11和C++ 14

C++ 2.0新特性包括了語言和標準庫兩個方面,標準庫主要是以標頭檔案的形式呈現

標頭檔案不帶 (.h), 例如 #include <vector>
新式的C 標頭檔案也不帶 (.h), 例如 #include<cstdio>
新式的C 標頭檔案帶有 (.h) 的標頭檔案仍然可用, 例如 #include <stdio.h>

一些新增的標頭檔案

#include <type_traits>
#include <unordered_set>
#include <forward_list>
#include <array>
#include <tuple>
#include <regex>
#include <thread>

語言

Variadic Templates

這個特性的典型應用就是提供泛型程式碼處理任意數量任意型別的引數。而具有這種特性的模板被稱為可變引數模板。(注意"..."出現的地方,此為語法規定)

#include <bits/stdc++.h>
using namespace std;
template <typename T>
void print() { // one 當引數個數為0的時候呼叫
}
template <typename T, typename... Types>
void print(const T& firstArg, const Types&... args) { // two (注意新增const)
    //cout << sizeof...(Types) << endl;
    //cout << sizeof...(args) << endl; // 列印args引數的個數,上述兩種方式結果相同
    cout << firstArg << endl;            // 列印第一個引數
    print(args...);             // 列印其餘引數,
}
int main() {
  print(7.5, "hello", bitset<16>(377), 42);
}


遞迴呼叫方法print函式,當剛開始傳入的時候使用列印第一個引數,然後使用print(T& firstArg, Types&... args)將函式引數包中引數的內容一一列印,當函式引數包中的沒有引數時候,呼叫print()結束遞迴。
可以這樣理解:剛開始比如傳入的是5個引數,則進入 two 的時候變成了(1,4),然後 one 列印第一個引數的內容,接著繼續呼叫 two, 引數變成了(1,3),然後重複以上內容,直到進入 two 的時候變成了(1,0),這個時候引數為空的print(),結束遞迴。

問題:既然sizeof可以判斷args的引數個數,那我們是不是可以把列印一個引數的函式略去,直接判斷如果args為0的話,直接return呢?

答案是很遺憾,這樣不行,就上面的問題寫出程式碼:

void print(const T& firstArg, const Types&... args) { // two (注意新增const)
    cout << firstArg << endl;            // 列印第一個引數
    if(sizeof...(args) > 0) 
       print(args...);             // 列印其餘引數
}

上述程式碼不能通過編譯。被例項化的程式碼是否有效是執行時決定的, 然而函式呼叫時的例項化卻是編譯時的事情。也就是說,if 語句是否執行是執行時的事情,而編譯的時候每一次函式呼叫都會被例項化。因此,當我們呼叫print()列印最後一個引數時,print(args...)仍然會例項化,而我們並沒有提供不接受任何引數的print()函式,那麼就產生了錯誤。
然而在C++ 17中卻解決了這個問題:(介紹的是C++ 2.0的特性,這裡不過多介紹)

template <typename T, typename... Types>
void print(const T& firstArg, const Types&... args) { // two (注意新增const)
    cout << firstArg << endl; // 列印第一個引數
    if constexpr(sizeof...(args) > 0)
     print(args...);             // 列印其餘引數
}

遞迴繼承

template <typename... Values> class tuple;
template <> class tuple<> { };

//private tuple <Tail...> 完成遞迴繼承
template <typename Head, typename... Tail>
class tuple<Head, Tail...> : private tuple <Tail...> {
	typedef tuple<Tail...> inherited;
public:
	tuple() { }
	tuple(Head v, Tail... vtail) : m_head(v), inherited(vtail...) { }    // 呼叫base ctor並給予引數
	...
protected:
	Head m_head;
};

Spaces in Template Expression(模板表示式中的空格)

在C++11之前,多層模板引數在巢狀時,最後的兩個 >之間要加一個空格,以避免和 >>運算子混淆。

vector<list<int> >;		// 所有版本均能通過編譯
vector<list<int>>;		// C++11之後能通過編譯

nullptr 和 std::nullptr

C++ 11之前指標可以賦值為0和NULL,C++ 11之後,nullptr可以被自動轉換為任意指標型別,但不會被轉換為整型數。

// 函式兩個過載版本
void f(int);
void f(void*);
// 呼叫函式
f(0);		// 呼叫 f(int)
f(NULL);	// 若NULL被定義為0,則會呼叫 f(int),產生二義性
f(nullptr);	// 呼叫 f(void*)

Automatic Type Deduction with auto (使用auto自動推斷型別)

使用auto定義變數不用顯示指定型別,auto會自動做實參推導, 前提是必須有東西可以推,也就是必須 = 存在;

auto s = 10; // auto 為 int型別
double f(....){};
auto s = f(); // auto 為 int型別

list<string> c;
....
list<string>::iterator it;
it = find(c.begin(), c.end(), target);
使用auto
list<string> c;
....
auto it = find(c.begin(), c.end(), target);

可以看出使用auto可以使冗餘的程式碼簡單化。

Uniform Initialization(一致性的初始化)

在C++11之前,變數的初始化方式有括號(),花括號{}和賦值運算子=.因此C++ 11引入了一種uniform initialization機制,上述用到的初始化方式都可以用一個{}代替.

int values[]{1, 2, 3};
vector<int> v{2, 3, 5, 7, 11, 13, 17};
vector<string> cities{"Berlin", "New York", "London", "Braunschweig" "Cairo", "Cologne"};
complex<double> c{4.0, 3.0}; 	// 等價於 c(4.0, 3.0)

※ 其實是利用了一個事實:編譯器看到{t1,t2,...}便做出一個initializer_list, 它的內部是一個array<T,n>, 呼叫函式(例如:ctor)時該array內的元素可被編譯器逐一傳給函式,如果函式引數是initializer_list型別,且要傳入的物件(ctor)本身有引數為initializer_list 的建構函式,則整包傳入。(所有容器皆有這樣的ctor),如果沒有的話需要一一傳入。
例如:

class P {
public:
    // 有兩個過載版本的建構函式,uniform initialization時優先呼叫接收initializer list的過載版本
    P(int a, int b) {
        cout << "P(int, int), a=" << a << ", b=" << b << endl;
    }

    P(initializer_list<int> initlist) {
        cout << "P(initializer list<int>), values= ";
        for (auto i : initlist)
            cout << i << ' ';
        cout << endl;
    }
};

P p(77, 5);		// P(int, int), a=77, b=5
P q{77, 5};		// P(initializer list<int>), values= 77 5
P r{77, 5, 42}; // P(initializer list<int>), values= 77 5 42
P s = {77, 5};	// P(initializer list<int>), values= 77 5

上面的案例可以看出剛才的事實的正確性,如果沒有P(initializer_list initlist) {...}建構函式,則q{77,55}則就不能整包輸入,需要拆分,這個時候正好P(int a, int b) {...}也為兩個引數,則直接呼叫它,s也是一樣的,但是r不成立。

int i;		// i 未定義初值
int j{};	// j 初值為0
int* p;		// P 未定義初值
int* q{};	// q 初值為nullptr

uniform initialization可以防止窄化的功能,當然在不同的編譯器上提示資訊是不一樣的,有的會報錯,而有的只會警告。
uniform initialization底層依賴於模板類initializer_list

void print(std::initializer_list<int> vals) {
    for (auto p = vals.begin(); p != vals.end(); ++p) { //a list of values
        std::cout << *p << endl;
    }
}
print({12, 3, 5, 7, 11, 13, 17});

測試樣例:

#include <bits/stdc++.h>
using namespace std;
int main() {
   auto s = {1,2,3};
   cout << typeid(s).name() << endl;
}

輸出結果:

STL中的很多容器和演算法相關函式均有接收initializer list的過載版本,以vector、min和max為例:

#include <initializer_list>

vector(initializer_list<value_type> __l, 
       const allocator_type &__a = allocator_type()) 
    : _Base(a) 
    { _M_range_initalize(__l.begin(), __l.end(), random_access_iterator_tag()); }

vector &operator=(initalizer_list <value_type> __l) {
    this->assign(__l.begin(), __l.end());
    return *this;
}

void insert(iterator __position, initializer_list<value_type> __l) {
    this->insert(__position, __l.begin(), __l.end());
}

void assign(initializer_list<value_type> __l) { 
    this->assign(__l.begin(), __l.end()); 
}
vector<int> v1{2, 5, 7, 13, 69, 83, 50};
vector<int> v2({2, 5, 7513, 69, 83, 50});
vector<int> v3;
v3 = {2, 5, 7, 13, 69, 83, 50};
v3.insert(v3.begin() + 2, {0, 1, 2, 3, 4});

for (auto i : v3)
    cout << i << ' ';
cout << endl; // 2 5 0 1 2 3 4 7 13 69 83 50

cout << max({string("Ace"), string("Stacy"), string("Sabrina"), string("Bark1ey")}); //Stacy,可以輸入任意個數的引數,但必須保證引數型別已知
cout << min({string("Ace"), string("Stacy"), string("Sabrina"), string("Sarkley")}); //Ace
cout << max({54, 16, 48, 5});  //54
cout << min({54, 16, 48, 5});  //5

expecilit

  • expecilit極大多數情況下是隻用於建構函式前面

expecilit for ctors taking more than one argument //C++ 2.0 之前
expecilit for ctors taking more than one argument //c++ 2.0之後
當為expecilit for ctors taking more than one argument的時候

struct Complex {
   int real, imag;
   Complex(int re, int im = 0): real(re), imag(im){}  //單一實參
   Complex operator+(const Complex& x) {
     return Complex(real + x.real, imag + x.imag);
   }
};
int main() {
   Complex c1(12,5);
   Complex c2 = c1 + 5; //這個地方,5隱式轉換為Complex物件
}
加上expecilit
struct Complex {
   int real, imag;
   expecilit Complex(int re, int im = 0): real(re), imag(im){}  //加上expecilit的目的是:只可以顯示的呼叫,不能隱式的呼叫
   Complex operator+(const Complex& x) {
     return Complex(real + x.real, imag + x.imag);
   }
};
int main() {
   Complex c1(12,5);
   Complex c2 = c1 + 5;  // 此時會報錯
}

只有no expecilit one argument,這個時候才可以做隱式轉換。
當為expecilit for ctors taking more than one argument的時候,則可以用於任意多的引數的情況。

range-based for statement(基於範圍的宣告)

for( decl : coll) {
   statement
}
//coll 是容器, 把coll中的元素一個個賦值給左邊的decl

= default 和 = delete

=default
預設的函式宣告式是一種新的函式宣告方式,C++ 11允許新增= default關鍵字新增到預設函式宣告的末尾,這個預設函式有限制:預設建構函式,移動建構函式,賦值建構函式,移動賦值函式,解構函式,以將該函式宣告為顯示預設函式。這就使得編譯器為顯示預設函式生成了預設實現,它比手動程式設計函式更加有效。
假如我們實現一個有參建構函式,或者自己實現一個預設建構函式,那編譯器就不會建立預設的建構函式。

問題:那我什麼我們不自己實現一個預設建構函式呢?不是應該和編譯器為我們的預設建構函式一樣嗎?

儘管兩者在我們看來沒什麼不同,但使用=default實現的預設建構函式仍然有一定的好處。以下幾點做了一定的解釋:

  • 如果您希望您的類是聚合型別或普通型別(或通過傳遞性,POD型別),那麼需要使用’= default’,如果宣告一個子類物件,那子類的建構函式會呼叫父類的建構函式,但是自己定義的預設建構函式只是一個空的函式,而編譯器為我們生成的預設建構函式內部是會自動為我們呼叫父類的預設建構函式的。
  • 使用’= default’也可以與複製建構函式和解構函式一起使用。例如,空拷貝建構函式與預設拷貝建構函式(將執行其成員的複製副本)不同。對每個特殊成員函式統一使用’= default’語法使程式碼更容易閱讀。
class A {
public:
    A() {}			// 手動新增的空參建構函式
    A(int mem) : member(mem) {}
private:
    int member;
};

class B {
public:
    B() = default;	// 使用編譯器生成的空參建構函式
    B(int mem) : member(mem) {}
private:
    int member;
};

int main() {
    cout << std::is_pod<A>::value << endl;		// false
    cout << std::is_pod<B>::value << endl;		// true
    return 0;
}

=delete
在C++ 11之前,操作符delete 只有一個目的,即釋放已動態分配的記憶體。而C ++ 11標準引入了此操作符的另一種用法,即:禁用成員函式的使用。這是通過附加= delete來完成的; 說明符到該函式宣告的結尾。

  • 禁用拷貝建構函式
class A { 
public: 
    A(int x): m(x) { } 
    A(const A&) = delete;      
    A& operator=(const A&) = delete;  
    int m; 
}; 
  
int main() { 
    A a1(1), a2(2), a3(3); 
    a1 = a2;   // Error:the usage of the copy assignment operator is disabled 
    a3 = A(a2);  // Error:the usage of the copy constructor is disabled 
    return 0; 
}
  • 禁用解構函式(慎用)
class A { 
public: 
   A() = default;
   ~A() = delete;
}; 
  
int main() { 
   A a; //error: A destructor is deleted
   A *a = new A(); //ok
   delete a;  //error: A destructor is deleted
}
  • 禁用引數轉換
class A { 
public: 
    A(int) {} 
    A(double) = delete;  
}; 
int main() { 
    A A1(10); 
    // Error, conversion from  double to class A is disabled. 
    A A2(10.1);  
    return 0; 
} 

※ 刪除的函式定義必須是函式的第一個宣告, 不可以再類內宣告完,到了類外在新增‘=delete’。
刪除函式的優點:

  • 可以防止編譯器生成那些我們不想要的函式
  • 可以防止那些不必要的型別轉換。

在C++11之前的做法通常是將這些函式宣告為private函式,這樣外界就不能呼叫這些函式了.但是這種做法對友元的支援不好。

拿禁用拷貝複製

class PrivateCopy {
private:
    // C++11之前的做法,拷貝賦值函式僅能被內部和友元呼叫
    PrivateCopy(const PrivateCopy &);
    PrivateCopy &operator=(const PrivateCopy &);
	// other members
public:
    PrivateCopy() = default; 	// use the synthesized default constructor
    ~PrivateCopy(); 			// users can define objects of this type but not copy them
};

alias template(別名模板)

alias template使用關鍵字using

template<typename T>
using Vec = std::vector<T, MyAlloc<T>>;		// 使用alias template語法定義含有自定義分配器的vector
Vec<int> container;		// 使用Vec型別

上述功能使用巨集定義或typedef都不能實現

  • 要想使用巨集定義實現該功能,從語義上來說,應該這樣實現:
#define Vec<T> std::vector<T, MyAlloc<T>>		// 理想情況下應該這樣寫,但不能通過編譯
Vec<int> container;

但是define不支援以小括號定義引數,要想符合語法,需要這樣寫

#define Vec(T) std::vector<T, MyAlloc<T>>		// 能通過編譯,但是使用小括號失去了泛型的語義
Vec(int) container;

這樣可以通過編譯,但是Vec(int)這種指定泛型的方式與原生指定泛型的方式不一致.

  • typedef根本不接受引數,因此也不能實現上述功能.
    這個時候就用到了模板模板引數

模板模板引數

template<typename T, template<typename T> class Container>
class XCls {
private:
    Container<T> c;
public:
    // ...
};

// 錯誤寫法:
XCls<string, list> mylst2;		// 錯誤:雖然list的第二模板引數有預設值,但是其作模板模板引數時不能自動推導

// 正確寫法: 使用alias template指定第二模板引數
template<typename T>
using LST = list<T, allocator<T>>
XCls<string, list> mylst2;		// 正確:模板LST只需要一個模板引數9
  • 可以看到使用化名模板不單單只是少寫幾個單詞這麼簡單.

type alias

其用法類似於typedef.

noexcept

當只要函式不發出異常,就為它加上noexcept宣告

int f(int x) throw(); // f 不會丟擲異常, C++ 98風格
int f(int x) noexcept;  // f 不會丟擲異常, C++ 11風格
// noexcept也可以加條件比如上面的 noexcept = noexcept(true)
template<typename T, size_t N>
void swap(T (&a)[N], T(&b)[N]) noexcept(noexcept(swap(*a, *b)));  // 當swap(*a, *b) 不丟擲異常的時候,才能保證函式不丟異常
  • 在vector中的移動建構函式必須加上noexcept,因為在vector成長的時候如果移動建構函式沒有加noexcept的話就沒法使用

override

override:複寫,改寫,用於虛擬函式
基類中定義了一些虛擬函式,派生類中對於虛擬函式的繼承可能出現漏掉一些條件的情況,然而編譯器有的是不會報錯的,但是如果我們在派生類繼承而來的虛擬函式後面加上override,編譯器會嚴格檢測派生類,如果你沒有按照基類中的形式寫的話,會報錯.

class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
void mf4() const;
};
class Derived: pulic Base {
public:
virtual void mf1();  //缺少const
virtual void mf2(unsigned int x); // 引數型別不正確
virtual void mf3() &&;  //左值變右值呼叫
void mf4() const;
}
// 上面的派生類繼承自基類的方法因缺少一些修飾而沒有達到我們的目的,但是編譯器不會報錯
// 變成下面這樣編譯器會嚴格檢查:
class Derived: pulic Base {
public:
virtual void mf1() override;  //缺少const
virtual void mf2(unsigned int x) override; // 引數型別不正確
virtual void mf3() && override;  //左值變右值呼叫
virtual void mf4() const override;
}

final

修飾類的時候

struct Base1 final{...}; //這個地方是告訴編譯器Base1不可以被繼承,它是體系當中的最後一個
struct Deviced : Base1{...}; //報錯:Base1不可被繼承

修飾類中的方法

struct Base1 {
  virtual f() final;
}; 
struct Deviced : Base1{
  void f(); //報錯:f()不可被重寫
}; 

decltype

decltype主要用於推導表示式的型別,作用類似於C++ 11之前的typeof
下面用來描述decltype的三大應用

  • 用於描述返回型別
    下面程式使用decltype宣告函式add的返回值型別:
template <typename T1, typename T2>
decltype(x+y) add(Tl x, T2 y);			// error: 'x' and 'y' was not declared in this scope

從語法上來說,上述程式是錯誤的,因為變數x和y在函式外訪問不到,因此需要使用C++11宣告返回值型別的新語法:

template<typename T1, typename T2>
auto add(T1 x, T2 y) ->decltype(x+y); //前提是x和y可以相加
  • 用於超程式設計(其實就是模板中的運用)
template <typename T>
void test_decltype(T obj) {

    map<string, float>::value_type elem1; 	
	
    typedef typename decltype(0bj)::iterator iType;
	typedef typename T::iterator iType;

    decltype(obj) anotherObj(obj);
}

這個地方需要再介紹一下typename的兩個作用:第一個作用是和class一樣宣告模板的模板引數,定義template , 第二個作用是使用巢狀依賴型別,如下, 這個時候typename後面的字串為一個型別名稱,而不是成員函式或者成員變數,如果前面沒有typename,編譯器沒有任何辦法知道T::LengthType是一個型別還是一個成員名稱(靜態資料成員或者靜態函式),所以編譯不能夠通過,所以加上typename就是為了指出這是一個型別名, 用於有::操作符的地方。

class MyArray      
   {      
   public:
       typedef   int   LengthType;
       .....
   }

   template<class T>
   void MyMethod( T myarr ) 
   {          
       typedef typename T::LengthType LengthType;        
       LengthType length = myarr.GetLength; 
   }
  • 代指lambda函式的型別
    面對lambda表示式,一般我們手上只有object,沒有type,要獲得type就要藉助decltype
// 定義lambda函式,lambda函式作為變數的變數型別較複雜,因此使用auto進行推斷
auto cmp = [](const Person &p1, const Person &p2) {
    return p1.lastname() < p2.lastname() ||
           (p1.lastname() == p2.lastname() && p1.firstname() < p2.firstname());
};
// 使用decltype語法推斷lambda函式cmp的型別
std::set<Person, decltype(cmp)> coll(cmp);

lambda函式

lambda函式既可以用作變數,也可以立即執行:

[] {
    std::cout << "hello lambda" << std::endl;
};

// 用作變數
auto l = [] {
    std::cout << "hello lambda" << std::endl;
};
l();

// 直接執行
[] {
    std::cout << "hello lambda" << std::endl;
}();

lambda函式的完整語法如下:

  • [...] (...) mutableopt  throwSpeCopt -> retTypeopt {...}
    其中mutableopt,throwSpeCopt, retTypeopt都是可選的.

[...]部分指定可以在函式體內訪問的外部非static物件,可以通過這部分訪問函式作用域外的變數.

  • [=]表示使用值傳遞變數.
  • [&]表示使用引用傳遞變數.
int id = 0;
auto f = [id]() mutable {
    std::cout << "id:" << id << std::endl;
    ++id;
};
id = 42;
f();							// id:0
f();							// id:1
f();							// id:2
std::cout << id << std::endl;	// 42

lambda函式使用時相當於仿函式(functor),[...]中傳入的物件相當於為仿函式的成員變數.

class Functor {
    private:
    int id; // copy of outside id
    public:
    void operator()() {
        std::cout << "id: " << id << std::endl;
        ++id; // OK
    }
};
Functor f;

與STL結合時,相比於仿函式,lambda函式通常更優雅:

// lambda函式充當predict謂詞
vector<int> vi{5, 28, 50, 83, 70, 590, 245, 59, 24};
int x = 30;
int y = 100;
remove_if(vi.begin(), vi.end(),
          	[x, y](int n) { return x < n && n < y; });
// 仿函式充當predict謂詞
class LambdaFunctor {
public:
    LambdaFunctor(int a, int b) : m_a(a), m_b(b) {}

    bool operator()(int n) const {
        return m_a < n && n < m_b;
    }

private:
    int m_a;
    int m_b;
};

remove_if(vi.begin(), vi.end(),
          	LambdaFunctor(x, y));

繼續介紹variadic template

void PrintX(){}
template<typename T, typename... Types>
void Printx(const T& firstArg, const Types&... args) {
   cout << firstArg << endl;
   PrintX(args...);
}

int main() {
  PrintX(77, "string", bitset<30>(377) , 100);
}

有6個案例演示variadic template的強大之處

1. 定義printX()函式輸出任意數目任意型別的變數

// 過載版本1,用於結束遞迴
void printX() {
}

// 過載版本2,先輸出第一個引數,再遞迴呼叫自己處理餘下的引數
template<typename T, typename... Types>
void printX(const T &firstArg, const Types &... args) {
    cout << firstArg << endl;
    printX(args...);
}

// 過載版本3,可以與過載版本2並存麼?
template<typename... Types>
void printX(const Types &... args) {
}

對於PrintX(77, "string", bitset<30>(377) , 100);的執行順序,我在上面已經介紹。

對於上面的問題是過載版本2和過載版本3可以共存,但是過載版本3不會被呼叫,因為過載版本2比過載版本3更特化

2. 重寫printf()函式

void printf(const char *s) {
    while (*s) {
        if (*s == '%' && *(++s) != '%')
            throw std::runtime_error("invalid format string: missing arguments");
        std::cout << *s++;
    }
}

template<typename T, typename... Args>
void printf(const char *s, T value, Args... args) {
    while (*s) {
        if (*s == '%' && *(++s) != '%') {
            std::cout << value;
            printf(++s, args...); // call even when *s = 0 to detect extra arguments
            return;
        }
        std::cout << *s++;
    }
    throw std::logic_error("extra arguments provided to printf");
}
int main() {
   int* pi = new int;
   printf("params:%d %s %p %f \n", 15, "This is Ace.", pi, 3.141592653);
}

3. 重寫max()函式接收任意引數

max()函式的所有引數的型別相同的話,直接使用initializer_list傳遞引數即可.

std::max({10.0, 20.0, 4.5, 8.1});
  • 使用initializer_list的一個限制就是引數型別必須相同,否則會報錯
    然而使用variadic template重寫max函式使之接受任意型別引數:
int maximum(int n) {
    return n;
}

template<typename... Args>
int maximum(int n, Args... args) {
    return std::max(n, maximum(args...));
}

4. 過載tuple的<<運算子,以異於一般的方式處理頭尾元素

// helper: print element with index IDX of the tuple with MAX elements
template<int IDX, int MAX, typename... Args>
struct PRINT_TUPLE {
    static void print(ostream &os, const tuple<Args...> &t) {
        os << get<IDX>(t) << (IDX + 1 == MAX ? "" : ",");
        PRINT_TUPLE<IDX + 1, MAX, Args...>::print(os, t);
    }
};

// partial specialization to end the recursion
template<int MAX, typename... Args>
struct PRINT_TUPLE<MAX, MAX, Args...> {
    static void print(std::ostream &os, const tuple<Args...> &t) {
    }
};

// output operator for tuples
template<typename... Args>
ostream &operator<<(ostream &os, const tuple<Args...> &t) {
    os << "[";
    PRINT_TUPLE<0, sizeof...(Args), Args...>::print(os, t);
    return os << "]";
}

5. 遞迴繼承實現tuple容器

// 定義 tuple類
template<typename... Values>
class tuple;

// 特化模板引數: 空參
template<>
class tuple<> {};

// 特化模板引數
template<typename Head, typename... Tail>
class tuple<Head, Tail...> :
        private tuple<Tail...>        	// tuple類繼承自tuple類,父類比子類少了一個模板引數
{
    typedef tuple<Tail...> inherited;	// 父類型別  
protected:
    Head m_head;						// 儲存第一個元素的值
public:
    tuple() {}
    tuple(Head v, Tail... vtail)		// 建構函式: 將第一個元素賦值給m_head,使用其他元素構建父類tuple
		: m_head(v), inherited(vtail...) {}

    Head head() { return m_head; }		// 返回第一個元素值
    inherited &tail() { return *this; }	// 返回剩餘元素組成的tuple(將當前元素強制轉換為父類型別)
};

6. 遞迴複合實現tuple容器

template<typename... Values>
class tup;

template<>
class tup<> {};

template<typename Head, typename... Tail>
class tup<Head, Tail...> {
    typedef tup<Tail...> composited;
protected:
    composited m_tail;
    Head m_head;
public:
    tup() {}
    tup(Head v, Tail... vtail) : m_tail(vtail...), m_head(v) {}
    
    Head head() { return m_head; }
    composited &tail() { return m_tail; }
};

標準庫

move

可以理解為完成容器的一些加速操作

平時實現物件的複製的時候,用賦值建構函式(copy)申請空間,然後指標指向新分配的空間,然後完成賦值,而移動建構函式(move)卻是用一個新的指標指向原本需要賦值的物件,略去了一些行為,完成對物件的加速,但是因為新的指標指向了需要複製的物件,所以原本指向該物件的指標就不能使用。

  • 右值引用:只能出現的=的右邊
  • 左值引用:可以放在=兩側
    如果物件是一個左值,可以用std::move(物件),來強行把它變成一個右值
int foo() { return 5; }

int x = foo();
int *p = &foo();	// lvalue required as unary '&' operand
foo() = 7;			// lvalue required as left operand of assignment

臨時物件是一個右值,不可以取地址。
假設對於自定義的一個類,放到容器當中,這裡拿vector舉例,當我們呼叫insert方法的時候,運用的是vector中的insert(...&&自定義類) 方法,然後這個方法內中呼叫的應該是自定義類中的移動建構函式。
呼叫中間函式(這個地方可以看做上面的insert方法)會改變變數的可變性和左值右值等性質,導致引數的非完美轉交(unperfect forwarding),下面程式中的中間轉交函式

perfect forwarding

// 函式process的兩個過載版本,分別處理引數是左值和右值的情況
void process(int &i) {
    cout << "process(int&):" << i << endl;
}
void process(int &&i) {
    cout << "process(int&&):" << i << endl;
}

// 中間轉交函式forward接收一個右值,但函式內將其作為左值傳遞給函式process了
void forward(int &&i) {
    cout << "forward(int&&):" << i << ", ";
    process(i); 
}

上面的forward(..)方法破壞了引數本身是一個右值的性質

int a = 0;
process(a);				// process(int&):0   	(變數作左值)
process(1);				// process(int&&):1		(臨時變數作右值)
process(std::move(a)); 	// process(int&&):0		(使用std::move將左值改為右值)
forward(2); 			// forward(int&&):2, process(int&):2	(臨時變數作左值傳給forward函式,forward函式體內將變數作為右值傳給process函式)
forward(std::move(a)); 	// forward(int&&):0, process(int&):0	(臨時變數作左值傳給forward函式,forward函式體內將變數作為右值傳給process函式)

forward(a);         	// ERROR: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int'
const int &b = 1;
process(b);         	// ERROR: binding reference of type 'int&' to 'const int' discards qualifiers
process(move(b));       // ERROR: binding reference of type 'int&&' to 'std::remove_reference<const int&>::type' {aka 'const int'} discards qualifiers

使用std::forward()函式可以完美轉交變數,不改變其可變性和左值右值等性質:

// 函式process的兩個過載版本,分別處理引數是左值和右值的情況
void process(int &i) {
    cout << "process(int&):" << i << endl;
}
void process(int &&i) {
    cout << "process(int&&):" << i << endl;
}

// 中間轉交函式forward使用std::forward()轉交變數
void forward(int &&i) {
    cout << "forward(int&&):" << i << ", ";
    process(std::forward<int>(i));
}
forward(2);           	// forward(int&&):2, process(int&&):2	(臨時變數作左值傳給forward函式,forward函式體內使用std::forward函式包裝變數,保留其作為右值的性質)
forward(std::move(a));  // forward(int&&):0, process(int&&):0	(臨時變數作左值傳給forward函式,forward函式體內使用std::forward函式包裝變數,保留其作為右值的性質)

move-aware class

編寫一個支援move語義的類MyString以演示移動建構函式和移動賦值函式的寫法.

#include <cstring>

class MyString {
public:
    static size_t DCtor;    // 累計預設建構函式呼叫次數
    static size_t Ctor;     // 累計建構函式呼叫次數
    static size_t CCtor;    // 累計拷貝建構函式呼叫次數
    static size_t CAsgn;    // 累計拷貝賦值函式呼叫次數
    static size_t MCtor;    // 累計移動建構函式呼叫次數
    static size_t MAsgn;    // 累計移動賦值函式呼叫次數
    static size_t Dtor;     // 累計解構函式呼叫次數
private:
    char *_data;
    size_t _len;

    void _init_data(const char *s) {
        _data = new char[_len + 1];
        memcpy(_data, s, _len);
        _data[_len] = '\0';
    }

public:
    // 預設建構函式
    MyString() : _data(nullptr), _len(0) { ++DCtor; }

	// 建構函式
    MyString(const char *p) : _len(strlen(p)) {
        ++Ctor;
        _init_data(p);
    }

    // 拷貝建構函式
    MyString(const MyString &str) : _len(str._len) {
        ++CCtor;
        _init_data(str._data);
    }

    // 拷貝賦值函式
    MyString &operator=(const MyString &str) {
        ++CAsgn;
        if (this != &str) {
            if (_data) delete _data;

            _len = str._len;
            _init_data(str._data); //COPY!
        }
        return *this;

    }

    // 移動建構函式
    MyString(MyString &&str) noexcept : _data(str._data), _len(str._len) {
        ++MCtor;
        str._len = 0;
        str._data = nullptr; 	// 將傳入物件的_data指標設為nullptr,防止解構函式多次delete同一根指標
    }

	// 移動賦值函式
    MyString &operator=(MyString &&str) noexcept {
        ++MAsgn;
        if (this != &str) {
            if (_data) delete _data;
            _len = str._len;
            _data = str._data; //MOVE!
            str._len = 0;
            str._data = nullptr; // 將傳入物件的_data指標設為nullptr,防止解構函式多次delete同一根指標
        }
        return *this;
    }

    //dtor
    virtual ~MyString() {
        ++Dtor;
        if (_data)
            delete _data;
    }
};

size_t MyString::DCtor = 0;
size_t MyString::Ctor = 0;
size_t MyString::CCtor = 0;
size_t MyString::CAsgn = 0;
size_t MyString::MCtor = 0;
size_t MyString::MAsgn = 0;
size_t MyString::Dtor = 0;

值得注意的有兩點:

  • 移動建構函式和移動賦值函式通常不涉及記憶體操作,不會丟擲異常,因此應加以noexcept修飾.
  • 在移動建構函式和移動賦值函式中,移動了原物件的資料後,要把原物件的資料指標置空,防止解構函式多次delete同一指標

測試move語義對容器的作用

move語義可以減少深拷貝,可以加速容器操作,編寫下述測試函式進行測試:

template<typename M, typename NM>
void test_moveable(M c1, NM c2, long &value) {
    char buf[10];

    // 測試儲存moveable物件的容器
    typedef typename iterator_traits<typename M::iterator>::value_type V1type;
    clock_t timeStart = clock();
    for (long i = 0; i < value; ++i) {
        snprintf(buf, 10, "%d", rand()); 	// 向容器內放入隨機字串
        auto ite = c1.end();				// 定位尾端
        c1.insert(ite, V1type(buf)); 		// 安插於尾端 (對RB-tree和HT這只是hint)
    }
    cout << "construction, milli-seconds: " << (clock() - timeStart) << endl;
    cout << "size()= " << c1.size() << endl;

	output_static_data(*(c1.begin()));
    // 測試容器的std::move()語義
    M c11(c1);
    M c12(std::move(c1));
    c11.swap(c12);

    // 對儲存non-movable物件的容器進行上述測試
    // ...
}

template<typename T>
void output_static_data(const T &myStr) {
    cout << typeid(myStr).name() << "-- " << endl;
    cout << "CCtor=" << T::CCtor
         << " MCtor=" << T::MCtor
         << "Asgn=" << T::CAsgn
         << "MAsgn=" << T::MAsgn
         << "Dtor=" << T::Dtor
         << "Ctor=" << T::Ctor
         << "DCtor=" << T::DCtor
         << endl;
}
long value = 3000000L;
test_moveable(vector<MyString>(), vector<MyStringNonMovable>(), value);
test_moveable(list<MyString>(), list<MyStringNonMovable>(), value);
test_moveable(deque<MyString>(), deque<MyStringNonMovable>(), value);
test_moveable(multiset<MyString>(), multiset<MyStringNonMovable>(), value);
test_moveable(unordered_multiset<MyString>(), unordered_multiset<MyStringNonMovable>(), value);

測試結果:

  • 在插入元素部分,只有vector容器的速度受元素是否movable影響大,這是因為只有容器vector在增長過程中會發生複製.
  • 對於所有容器,其移動建構函式都遠快於其拷貝建構函式,容器vector的移動複製函式僅僅發生了指標的交換,未發生元素的複製.

新的容器特性

容器array

內部其實就是一個陣列,初始化的是不必須指明型別和陣列個數:array<int,10>, 10個int型別的資料

  • array容器沒有建構函式和解構函式,因為array就是為了表現純粹的陣列。

容器hashtable

  • vector裡面存放的是單向連結串列
    hashtable最開始只有53個桶,當元素個數大於桶的個數時,桶的數目擴大為最接近當前桶數兩倍的質數,實際上,桶數目的增長順序被寫死在程式碼裡:
static const unsigned long __stl_prime_list[__stl_num_primes] = {
        53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
        49157, 98317, 196613, 393241, 786433, 1572869, 3145739,
        6291469, 12582917, 25165843, 50331653, 100663319,
        201326611, 402653189, 805306457, 1610612741,
        3221225473ul, 4294967291ul};

hash function

hash function的目的就是希望把元素值算出一個hash code(一個可執行modulus運算的值), 使得元素經hash code 對映之後能夠(夠亂夠隨意)地被至於hashtable內,越亂,與不容易發生碰撞。
以G2.9版關於C的char型別舉例


tuple

// 建立tuple
tuple<string, int, int, complex<double> > t;
tuple<int, float, string> t1(41, 6.3, "nico");	// 指定初值
auto t2 = make_tuple(22, 44, "stacy");			// 使用make_tuple函式建立tuple

// 使用get<>()函式獲取tuple內的元素
cout << "t1:" << get<0>(t1) << "<< get<1>(t1)<<" << get<2>(t1) << endl;
get<1>(t1) = get<1>(t2);		// 獲取的元素是左值,可以對其賦值


// tuple可以直接進行比較
if (t1 < t2) { 
    cout << "t1 < t2" << endl;
} else {
    cout << "t1 >= t2" << endl;
}

// 可以直接拷貝構造
t1 = t2; 

// 使用tie函式將tuple的元素繫結到變數上
tuple<int, float, string> t3(77, 1.1, "more light");
int i1, float f1; string s1;
tie(i1, f1, s1) = t3; 

// 推斷 tuple 型別
typedef decltype(t3) TupleType;		// 推斷出 t3 的型別為 tuple<int, float, string>

// 使用 tuple_size 獲取元素個數
cout << tuple_size<TupleType>::value << endl; 		// 3
// 使用 tuple_element 獲取元素型別
tuple_element<1, TupleType>::type fl = 1.0; 		// float

tuple類原始碼分析
容器tuple的原始碼使用可變模板引數,遞迴呼叫不同模板引數的tuple建構函式,以處理任意多的元素型別.

// 定義 tuple類
template<typename... Values>
class tuple;

// 特化模板引數: 空參
template<>
class tuple<> {};

// 特化模板引數
template<typename Head, typename... Tail>
class tuple<Head, Tail...> :
        private tuple<Tail...>        	// tuple類繼承自tuple類,父類比子類少了一個模板引數
{
    typedef tuple<Tail...> inherited;	// 父類型別  
protected:
    Head m_head;						// 儲存第一個元素的值
public:
    tuple() {}
    tuple(Head v, Tail... vtail)		// 建構函式: 將第一個元素賦值給m_head,使用其他元素構建父類tuple
		: m_head(v), inherited(vtail...) {}

    Head head() { return m_head; }		// 返回第一個元素值
    inherited &tail() { return *this; }	// 返回剩餘元素組成的tuple(將當前元素強制轉換為父類型別)
};


呼叫head函式返回的是元素m_head的值.
呼叫tail函式返回父類成分的起點,通過強制轉換將當前tuple轉換為父類tuple,丟棄了元素m_head所佔記憶體.

上述總結的是部分C++ 2.0特性,關於博文內容的視訊可以去看侯捷老師的C++新標準 11/14

相關文章