天天看點

C++ 11新特性 (三)

右值引用

C++11 增加了一個新的類型,稱為右值引用( R-value reference),标記為 &&

和聲明左值引用一樣,右值引用也必須立即進行初始化操作,且隻能使用右值進行初始化,比如:

int num = 10;
//int && a = num;  //右值引用不能初始化為左值
int && a = 10;      

和常量左值引用不同的是,右值引用還可以對右值進行修改。例如:

int && a = 10;
a = 100;
cout << a << endl;      

另外值得一提的是,C++ 文法上是支援定義常量右值引用的,例如

const int&& a = 10;//編譯器不會報錯      

學到這裡,一些讀者可能無法記清楚左值引用和右值引用各自可以引用左值還是右值,這裡給大家一張表格,友善大家記憶:

C++11 中右值可以分為兩種:一個是将亡值( xvalue, expiring value),另一個則是純右值( prvalue, PureRvalue):

  • 純右值:非引用傳回的臨時變量、運算表達式産生的臨時變量、原始字面量和 lambda 表達式等
  • 将亡值:與右值引用相關的表達式,比如,T&& 類型函數的傳回值、 std::move 的傳回值等。

    右值的使用:

    在 C++ 中在進行對象指派操作的時候,很多情況下會發生對象之間的深拷貝,如果堆記憶體很大,這個拷貝的代價也就非常大,在某些情況下,如果想要避免對象的深拷貝,就可以使用右值引用進行性能的優化。

#include <iostream>
using namespace std;

class Test
{
public:
    Test() : m_num(new int(100))
    {
        cout << "construct: my name is jerry" << endl;
    }

    Test(const Test& a) : m_num(new int(*a.m_num))
    {
        cout << "copy construct: my name is tom" << endl;
    }

    ~Test()
    {
        delete m_num;
    }

    int* m_num;
};

Test getObj()
{
    Test t;
    return t;
}

int main()
{
    Test t = getObj();
    cout << "t.m_num: " << *t.m_num << endl;
    return 0;
};
construct: my name is jerry
copy construct: my name is tom
t.m_num: 100      

過輸出的結果可以看到調用 Test t = getObj(); 的時候調用拷貝構造函數對傳回的臨時對象進行了深拷貝得到了對象 t,在 getObj() 函數中建立的對象雖然進行了記憶體的申請操作,但是沒有使用就釋放掉了。如果能夠使用臨時對象已經申請的資源,既能節省資源,還能節省資源申請和釋放的時間,如果要執行這樣的操作就需要使用右值引用了,右值引用具有移動語義,移動語義可以将資源(堆、系統對象等)通過淺拷貝從一個對象轉移到另一個對象這樣就能減少不必要的臨時對象的建立、拷貝以及銷毀,可以大幅提高 C++ 應用程式的性能。

#include <iostream>
using namespace std;

class Test
{
public:
    Test() : m_num(new int(100))
    {
        cout << "construct: my name is jerry" << endl;
    }

    Test(const Test& a) : m_num(new int(*a.m_num))
    {
        cout << "copy construct: my name is tom" << endl;
    }

    // 添加移動構造函數
    Test(Test&& a) : m_num(a.m_num)
    {
        a.m_num = nullptr;
        cout << "move construct: my name is sunny" << endl;
    }

    ~Test()
    {
        delete m_num;
        cout << "destruct Test class ..." << endl;
    }

    int* m_num;
};

Test getObj()
{
    Test t;
    return t;
}

int main()
{
    Test t = getObj();
    cout << "t.m_num: " << *t.m_num << endl;
    return 0;
};

construct: my name is jerry
move construct: my name is sunny
destruct Test class ...
t.m_num: 100
destruct Test class ...      

通過修改,在上面的代碼給 Test 類添加了移動構造函數(參數為右值引用類型),這樣在進行 Test t = getObj(); 操作的時候并沒有調用拷貝構造函數進行深拷貝,而是調用了移動構造函數,在這個函數中隻是進行了淺拷貝,沒有對臨時對象進行深拷貝,提高了性能。

如果不使用移動構造,在執行 Test t = getObj() 的時候也是進行了淺拷貝,但是當臨時對象被析構的時候,類成員指針 int* m_num; 指向的記憶體也就被析構了,對象 t 也就無法通路這塊記憶體位址了。

在測試程式中 getObj() 的傳回值就是一個将亡值,也就是說是一個右值,在進行指派操作的時候如果 = 右邊是一個右值,那麼移動構造函數就會被調用。移動構造中使用了右值引用,會将臨時對象中的堆記憶體位址的所有權轉移給對象t,這塊記憶體被成功續命,是以在t對象中還可以繼續使用這塊記憶體。

move 轉移

在 C++11 添加了右值引用,并且不能使用左值初始化右值引用,如果想要使用左值初始化一個右值引用需要借助 std::move () 函數,使用std::move方法可以将左值轉換為右值。使用這個函數并不能移動任何東西,而是和移動構造函數一樣都具有移動語義,将對象的狀态或者所有權從一個對象轉移到另一個對象,隻是轉移,沒有記憶體拷貝。

從實作上講,std::move 基本等同于一個類型轉換:static_cast<T&&>(lvalue);,函數原型如下:

template<class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) _NOEXCEPT
{   // forward _Arg as movable
    return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
}      

使用方法如下:

class Test
{
public:
    Test(){}
    ......
}
int main()
{
    Test t;
    Test && v1 = t;          // error
    Test && v2 = move(t);    // ok
    return 0;
}      

在第 4 行中,使用左值初始化右值引用,是以文法是錯誤的。

在第 5 行中,使用 move() 函數将左值轉換為了右值,這樣就可以初始化右值引用了。

list<string> ls;
ls.push_back("hello");
ls.push_back("world");
......
list<string> ls1 = ls;        // 需要拷貝, 效率低
list<string> ls2 = move(ls);      

如果不使用 std::move,拷貝的代價很大,性能較低。使用 move 幾乎沒有任何代價,隻是轉換了資源的所有權。如果一個對象内部有較大的堆記憶體或者動态數組時,使用 move () 就可以非常友善的進行資料所有權的轉移。另外,我們也可以給類編寫相應的移動構造函數(T::T(T&& another))和和具有移動語義的指派函數(T&& T::operator=(T&& rhs)),在構造對象和指派的時候盡可能的進行資源的重複利用,因為它們都是接收一個右值引用參數。

forward 轉發

右值引用類型是獨立于值的,一個右值引用作為函數參數的形參時,在函數内部轉發該參數給内部其他函數時,它就變成一個左值,并不是原來的類型了。如果需要按照參數原來的類型轉發到另一個函數,可以使用 C++11 提供的 std::forward () 函數,該函數實作的功能稱之為完美轉發。

// 函數原型
template <class T> T&& forward (typename remove_reference<T>::type& t) noexcept;
template <class T> T&& forward (typename remove_reference<T>::type&& t) noexcept;

// 精簡之後的樣子
std::forward<T>(t);      

下面通過一個例子示範一下關于 forward 的使用:

#include <iostream>
using namespace std;

template<typename T>
void printValue(T& t)
{
    cout << "l-value: " << t << endl;
}

template<typename T>
void printValue(T&& t)
{
    cout << "r-value: " << t << endl;
}

template<typename T>
void testForward(T && v)
{
    printValue(v);
    printValue(move(v));
    printValue(forward<T>(v));
    cout << endl;
}

int main()
{
    testForward(520);
    int num = 1314;
    testForward(num);
    testForward(forward<int>(num));
    testForward(forward<int&>(num));
    testForward(forward<int&&>(num));

    return 0;
}

l-value: 520
r-value: 520
r-value: 520

l-value: 1314
r-value: 1314
l-value: 1314

l-value: 1314
r-value: 1314
r-value: 1314

l-value: 1314
r-value: 1314
l-value: 1314

l-value: 1314
r-value: 1314
r-value: 1314      

清單初始化

關于 C++ 中的變量,數組,對象等都有不同的初始化方法,在這些繁瑣的初始化方法中沒有任何一種方式适用于所有的情況。為了統一初始化方式,并且讓初始化行為具有确定的效果,在 C++11 中提出了清單初始化的概念。

統一的初始化:

在 C++11 中,清單初始化變得更加靈活了,來看一下下面這段初始化類對象的代碼

#include <iostream>
using namespace std;

class Test
{
public:
    Test(int) {}
private:
    Test(const Test &);
};

int main(void)
{
    Test t1(520);
    Test t2 = 520; 
    Test t3 = { 520 };
    Test t4{ 520 };
    int a1 = { 1314 };
    int a2{ 1314 };
    int arr1[] = { 1, 2, 3 };
    int arr2[]{ 1, 2, 3 };
    return 0;
}      

聚合體

在 C++11 中,清單初始化的使用範圍被大大增強了,但是一些模糊的概念也随之而來,在前面的例子可以得知,清單初始化可以用于自定義類型的初始化,但是對于一個自定義類型,清單初始化可能有兩種執行結果:

#include <iostream>
#include <string>
using namespace std;

struct T1
{
    int x;
    int y;
}a = { 123, 321 };

struct T2
{
    int x;
    int y;
    T2(int, int) : x(10), y(20) {}
}b = { 123, 321 };

int main(void)
{
    cout << "a.x: " << a.x << ", a.y: " << a.y << endl;
    cout << "b.x: " << b.x << ", b.y: " << b.y << endl;
    return 0;
}

a.x: 123, a.y: 321
b.x: 10, b.y: 20      

非聚合體

對于聚合類型的類可以直接使用清單初始化進行對象的初始化,如果不滿足聚合條件還想使用清單初始化其實也是可以的,需要在類的内部自定義一個構造函數, 在構造函數中使用初始化清單對類成員變量進行初始化:

#include <iostream>
#include <string>
using namespace std;

struct T1
{
    int x;
    double y;
    // 在構造函數中使用初始化清單初始化類成員
    T1(int a, double b, int c) : x(a), y(b), z(c){}
    virtual void print()
    {
        cout << "x: " << x << ", y: " << y << ", z: " << z << endl;
    }
private:
    int z;
};

int main(void)
{
    T1 t{ 520, 13.14, 1314 };   // ok, 基于構造函數使用初始化清單初始化類成員
    t.print();
    return 0;
}      

另外,需要額外注意的是聚合類型的定義并非遞歸的,也就是說當一個類的非靜态成員是非聚合類型時,這個類也可能是聚合類型,比如下面的這個例子:

#include <iostream>
#include <string>
using namespace std;

struct T1
{
    int x;
    double y;
private:
    int z;
};

struct T2
{
    T1 t1;
    long x1;
    double y1;
};

int main(void)
{
    T2 t2{ {}, 520, 13.14 };
    return 0;
}      

可以看到,T1 并非一個聚合類型,因為它有一個 Private 的非靜态成員。但是盡管 T2 有一個非聚合類型的非靜态成員 t1,T2 依然是一個聚合類型,可以直接使用清單初始化的方式進行初始化。

最後強調一下 t2 對象的初始化過程,對于非聚合類型的成員 t1 做初始化的時候,可以直接寫一對空的大括号 {},這相當于調用是 T1 的無參構造函數。

std::initializer_list

在 C++ 的 STL 容器中,可以進行任意長度的資料的初始化,使用初始化清單也隻能進行固定參數的初始化,如果想要做到和 STL 一樣有任意長度初始化的能力,可以使用 std::initializer_list 這個輕量級的類模闆來實作。

先來介紹一下這個類模闆的一些特點:

  • 它是一個輕量級的容器類型,内部定義了疊代器 iterator 等容器必須的概念,周遊時得到的疊代器是隻讀的。
  • 對于 std::initializer_list 而言,它可以接收任意長度的初始化清單,但是要求元素必須是同種類型 T
  • 在 std::initializer_list 内部有三個成員接口:size(), begin(), end()。
  • std::initializer_list 對象隻能被整體初始化或者指派。

std::initializer_list,使用初始化清單 { } 作為實參進行資料傳遞即可。

#include <iostream>
#include <string>
using namespace std;

void traversal(std::initializer_list<int> a)
{
    for (auto it = a.begin(); it != a.end(); ++it)
    {
        cout << *it << " ";
    }
    cout << endl;
}

int main(void)
{
    initializer_list<int> list;
    cout << "current list size: " << list.size() << endl;
    traversal(list);

    list = { 1,2,3,4,5,6,7,8,9,0 };
    cout << "current list size: " << list.size() << endl;
    traversal(list);
    cout << endl;
    
    list = { 1,3,5,7,9 };
    cout << "current list size: " << list.size() << endl;
    traversal(list);
    cout << endl;

    
    // 直接通過初始化清單傳遞資料 //
    
    traversal({ 2, 4, 6, 8, 0 });
    cout << endl;

    traversal({ 11,12,13,14,15,16 });
    cout << endl;


    return 0;
}


current list size: 0

current list size: 10
1 2 3 4 5 6 7 8 9 0

current list size: 5
1 3 5 7 9

2 4 6 8 0

11 12 13 14 15 16      
  • std::initializer_list擁有一個無參構造函數,是以,它可以直接定義執行個體,此時将得到一個空的
  • std::initializer_list,因為在周遊這種類型的容器的時候得到的是一個隻讀的疊代器,是以我們不能修改裡邊的資料,隻能通過值覆寫的方式進行容器内部資料的修改。雖然如此,在效率方面也無需擔心,
  • std::initializer_list的效率是非常高的,它的内部并不負責儲存初始化清單中元素的拷貝,僅僅存儲了初始化清單中元素的引用。

作為構造函數參數 std::initializer_list

自定義的類如果在構造對象的時候想要接收任意個數的實參,可以給構造函數指定為 std::initializer_list 類型,在自定義類的内部還是使用容器來存儲接收的多個實參。

#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Test
{
public:
    Test(std::initializer_list<string> list)
    {
        for (auto it = list.begin(); it != list.end(); ++it)
        {
            cout << *it << " ";
            m_names.push_back(*it);
        }
        cout << endl;
    }
private:
    vector<string> m_names;
};

int main(void)
{
    Test t({ "jack", "lucy", "tom" });
    Test t1({ "hello", "world", "nihao", "shijie" });
    return 0;
}
jack lucy tom
hello world nihao shijie      

using關鍵字

//---------------------------------------test2 可以取代typedef了,而且更加靈活  
    using myIntVec = std::vector<int>;  
    void testUsing2()  
    {  
        myIntVec mvec = { 1, 2, 3, 4, 5 };  
        mvec.push_back(123);  
        for (int num : mvec)  
            printf("--- num:%d\n", num);  
      
        std::cout << is_same < std::vector<int>, myIntVec>::value << std::endl; // 1  
    }  
      
    template <typename T>  
    using MapStr = std::map<T, std::string>;  
    void testUsing3()  
    {  
        MapStr<int> intStrMap;  
        intStrMap.insert(make_pair(123, "aaa"));  
        intStrMap.insert(make_pair(456, "bbb"));  
      
        MapStr<std::string> strstrMap;  
        strstrMap.insert(make_pair("ccc", "ddd"));  
        strstrMap.insert(make_pair("eee", "fff"));  
    }      
//---------------------------------------test1  
using namespace std;  
  
class Base  
{  
public:  
    void menfcn()  
    {  
        cout << "Base function" << endl;  
    }  
  
    void menfcn(int n)  
    {  
        cout << "Base function with int" << endl;  
    }  
//private:   
//  void menfcn(std::string _name) {}//會讓基類using時報不可通路的錯  
};  
  
class Derived : private Base  
{  
public:  
    using Base::menfcn;//using聲明隻能指定一個名字,不能帶形參表,且基類的該函數不能有私有版本,否則編譯報錯  
                        //using父類方法,主要是用來實作可以在子類執行個體中調用到父類的重載版本  
    int menfcn(int num)  
    {  
        cout << "Derived function with int : "<< num << endl;  
        return num;  
    }  
};  
  
/* 
“隐藏”是指派生類的函數屏蔽了與其同名的基類函數,規則如下: 
1、如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual關鍵字,基類的函數将被隐藏(注意别與重載混淆) 
2、如果派生類的函數與基類的函數同名,并且參數也相同,但是基類函數沒有virtual關鍵字。此時,基類的函數被隐藏(注意别與覆寫混淆) 
使用了using關鍵字,就可以避免1的情況,是的父類同名函數在子類中得以重載,不被隐藏 
*/  
  
void testUsing1()  
{  
    Base b;  
    Derived d;  
    b.menfcn();  
    d.menfcn();//如果去掉Derived類中的using聲明,會出現錯誤:error C2660: 'Derived::menfcn' : function does not take 0 arguments      
    d.menfcn(123);  
    /* 
    Base function 
    Base function 
    Derived function with int : 123 
    */  
}