1. 程式人生 > >C++ 面試 C++ 11 新特性之右值引用與移動

C++ 面試 C++ 11 新特性之右值引用與移動

右值引用

什麼是左值,什麼是右值,簡單說左值可以賦值,右值不可以賦值。以下面程式碼為例,“ A a = getA();”該語句中a是左值,getA()的返回值是右值。

#include <iostream>

class A
{
public:
    A() { std::cout << "Constructor" << std::endl; }
    A(const A&) { std::cout << "Copy Constructor" << std::endl; }
    ~A() {}
};

static
A getA() { A a; return a; } int main() { A a = getA(); return 0; }

執行以上程式碼,輸出結果如下:

Constructor
Copy Constructor

可以看到A的建構函式呼叫一次,拷貝建構函式呼叫了一次,建構函式和拷貝建構函式是消耗比較大的,這裡是否可以避免拷貝構造?

C++11做到了這一點。

#include <iostream>

class A
{
public:
    A() { std::cout << "Constructor" << std
::endl; } A(const A&) { std::cout << "Copy Constructor" << std::endl; } A(const A&&) { std::cout << "Move Constructor" << std::endl; } ~A() {} }; static A getA() { A a; return a; } int main() { A a = getA(); return 0; }

執行以上程式碼,輸出結果:

Constructor
Move Constructor

這樣就沒有呼叫拷貝建構函式,而是呼叫移動構造。這裡並沒有看到移動構造的優點。

#include <iostream>
#include <vector>

class B
{
public:
    B() {}
    B(const B&) { std::cout << "B Constructor" << std::endl; }
};

class A
{
public:
    A(): m_b(new B()) { std::cout << "A Constructor" << std::endl; }
    A(const A& src) :
        m_b(new B(*(src.m_b)))
    { 
        std::cout << "A Copy Constructor" << std::endl;
    }
    A(A&& src) :
        m_b(src.m_b)
    {
        src.m_b = nullptr;
        std::cout << "A Move Constructor" << std::endl;
    }
    ~A() { delete m_b; }

private:
    B* m_b;
};

static A getA()
{
    A a;
    std::cout << "================================================" << std::endl;
    return a;
}

int main()
{
    A a = getA();
    std::cout << "================================================" << std::endl;
    A a1(a);

    return 0;
}

輸出:

A Constructor
================================================
A Move Constructor
================================================
B Constructor
A Copy Constructor

“ A a = getA();”呼叫的是A的移動構造,“ A a1(a); ”呼叫的是A的拷貝構造。A的拷貝構造需要對成員變數B進行深拷貝,而A的移動構造不需要,很明顯,A的移動構造效率高。

std::move

std::move語句可以將左值變為右值而避免拷貝構造,修改程式碼如下:

#include <iostream>
#include <vector>

class B
{
public:
    B() {}
    B(const B&) { std::cout << "B Constructor" << std::endl; }
};

class A
{
public:
    A(): m_b(new B()) { std::cout << "A Constructor" << std::endl; }
    A(const A& src) :
        m_b(new B(*(src.m_b)))
    { 
        std::cout << "A Copy Constructor" << std::endl;
    }
    A(A&& src) noexcept :
        m_b(src.m_b)
    {
        src.m_b = nullptr;
        std::cout << "A Move Constructor" << std::endl;
    }
    ~A() { delete m_b; }

private:
    B* m_b;
};

static A getA()
{
    A a;
    std::cout << "================================================" << std::endl;
    return a;
}

int main()
{
    A a = getA();
    std::cout << "================================================" << std::endl;
    A a1(a);
    std::cout << "================================================" << std::endl;
    A a2(std::move(a1));
    return 0;
}

執行以上程式碼,輸出結果:

A Constructor
================================================
A Move Constructor
================================================
B Constructor
A Copy Constructor
================================================
A Move Constructor

“ A a2(std::move(a1));”將a1轉換為右值,因此a2呼叫的移動構造而不是拷貝構造。

#include <iostream>
#include <vector>


class B
{
public:
    B() {}
    B(const B&) { std::cout << "B Constructor" << std::endl; }
};

class A
{
public:
    A(): m_b(new B()) { std::cout << "A Constructor" << std::endl; }
    A(const A& src) :
        m_b(new B(*(src.m_b)))
    { 
        std::cout << "A Copy Constructor" << std::endl;
    }
    A(A&& src) :
        m_b(src.m_b)
    {
        src.m_b = nullptr;
        std::cout << "A Move Constructor" << std::endl;
    }
    A& operator=(const A& src) noexcept
    {
        if (this == &src)
            return *this;

        delete m_b;
        m_b = new B(*(src.m_b));
        std::cout << "operator=(const A& src)" << std::endl;
        return *this;
    }
    A& operator=(A&& src) noexcept
    {
        if (this == &src)
            return *this;

        delete m_b;
        m_b = src.m_b;
        src.m_b = nullptr;
        std::cout << "operator=(const A&& src)" << std::endl;
        return *this;
    }
    ~A() { delete m_b; }

private:
    B* m_b;
};

static A getA()
{
    A a;
    std::cout << "================================================" << std::endl;
    return a;
}

int main()
{
    A a = getA();//移動構造
    std::cout << "================================================" << std::endl;
    A a1(a);//拷貝構造
    std::cout << "================================================" << std::endl;
    A a2(std::move(a1));//移動構造
    std::cout << "================================================" << std::endl;
    a2 = getA();//移動賦值
    std::cout << "================================================" << std::endl;
    a2 = a1;//拷貝賦值
    return 0;
}

輸出結果:

A Constructor
================================================
A Move Constructor
================================================
B Constructor
A Copy Constructor
================================================
A Move Constructor
================================================
A Constructor
================================================
A Move Constructor
operator=(const A&& src)
================================================
B Constructor
operator=(const A& src)

總之儘量給類新增移動構造和移動賦值函式,而減少拷貝構造和拷貝賦值的消耗。 移動構造,移動賦值要加上noexcept,用於通知標準庫不丟擲異常。

一般性:

右值引用

右值是一個行將銷燬的值,例如(i * 10)這種表示式的值。新標準中允許通過&&標識定義一個右值引用,將其繫結到一個右值上。但是,一個右值引用變數又是一個左值,因為它是一個變量了嘛。

std::cout<<"test rvalue reference:\n";
int j = 42;
int &lr = j;
//int &&rr = j; // Wrong. Can't bind a rvalue ref to a lvalue.
//int &lr2 = i * 42; // Wrong. Can't bind a lvalue ref to a rvalue.
const int &lr3 = j * 42;
int &&rr2 = j * 42;
//int &&rr3 = rr2; // Wrong. rr2 is a rvalue ref and rvalue ref is a lvalue.
int &lr4 = rr2;
std::cout<<j<<'\t'<<lr<<'\t'<<lr3<<'\t'<<rr2<<'\t'<<lr4<<std::endl;
std::cout<<"test rvalue ref done.\n"<<std::endl;

std::move

std::move函式的作用很簡單,就是獲得一個左值的右值引用,這樣我們就找到了一種途徑將一個右值引用繫結到一個左值上。

但是,使用std::move也意味著交出左值的控制權,之後就不能再使用這個左值了,因為使用std::move之後,無法對這個左值做任何保證。

std::cout<<"test std::move:\n";
std::string str5 = "asdf";
std::string &lr5 = str5;
std::string &&rr5 = std::move(str5);
rr5[0] = 'b';
lr5[1] = 'z';
std::cout<<rr5<<'\t'<<lr5<<'\t'<<str5<<std::endl;
std::cout<<"test std::move done.\n"<<std::endl;

移動構造

新標準中一些內建型別(如string)都實現了移動建構函式。所謂移動構造,就是接受一個右值引用,從而接受該右值引用所引用的物件,而沒有實際的大塊記憶體拷貝操作(可以想象成只拷貝了一個指標而不是整塊的記憶體)。呼叫移動建構函式的關鍵是要傳入一個相應的右值引用,這時上面提到的std::move函式就派上用場了。

std::cout<<"test move constructor:\n";
std::allocator<std::string> alloc;
size_t size = 5;
auto old_strs = alloc.allocate(size);
for(size_t i = 0; i < size; i++)
{
    alloc.construct(old_strs + i, "abcde");
}
std::cout<<"old_strs[0]: "<<old_strs[0]<<std::endl;
auto new_strs = alloc.allocate(size);
for(size_t i = 0; i < size; i++)
{
    alloc.construct(new_strs + i, std::move(*(old_strs + i)));
}
std::cout<<"new_strs[0]: "<<new_strs[0]<<std::endl;
std::cout<<"old_strs[0]: "<<old_strs[0]<<std::endl;
for(size_t i = 0; i < size; i++)
{
    alloc.destroy(old_strs + i);
}
alloc.deallocate(old_strs, size);
std::cout<<"test move constructor done.\n"<<std::endl;

呼叫移動建構函式之後,右值引用所繫結的物件保證可析構可銷燬的狀態。

定義自己的移動建構函式

上面說到了,移動建構函式的關鍵是接受一個右值引用,竊取該物件的內容為己所用(不拷貝),並且保證被竊取的物件保持可析構可銷燬的狀態。那麼,我們當然可以定義一個自己的移動建構函式。

一個整型陣列的定義如下:

class IntVec
{
public:
    IntVec() = default;
    IntVec(size_t capacity);
    IntVec(IntVec &rhs);
    IntVec(IntVec &&rhs) noexcept;
    IntVec &operator=(IntVec &&rhs) & noexcept;
    ~IntVec();

    int push_back(int val);
    void print_info();

    size_t capacity;
    size_t size;
    int *pointer;
};
IntVec::IntVec(IntVec &rhs)
{
    this->capacity = rhs.capacity;
    this->size = rhs.size;
    this->pointer = new int[this->capacity];
    for(size_t i = 0; i < size; i++)
        this->pointer[i] = rhs.pointer[i];
    std::cout<<"IntVect copy constructor.\n";
}

IntVec::IntVec(size_t capacity)
    : capacity(capacity), size(0)
{
    this->pointer = new int[capacity];
}

IntVec::IntVec(IntVec &&rhs) noexcept
    : capacity(rhs.capacity), size(rhs.size), pointer(rhs.pointer)
{
        rhs.pointer = nullptr;
        rhs.capacity = rhs.size = 0;
        std::cout<<"IntVect move constructor.\n";
}

IntVec &IntVec::operator=(IntVec &&rhs) & noexcept
{
    if(this != &rhs)
    {
        if(this->pointer)
            delete [] this->pointer;
        this->pointer = rhs.pointer;
        this->capacity = rhs.capacity;
        this->size = rhs.size;
        rhs.pointer = nullptr;
        rhs.capacity = rhs.size = 0;
    }
    std::cout<<"IntVect move assign constructor.\n";
    return *this;
}
IntVec::~IntVec()
{
    if(this->pointer)
        delete [] this->pointer;
}

push_back和print_info的定義就不贅述了。

可以看到,在移動建構函式裡,只需要竊取指標及其狀態,並將右值引用物件的狀態重置,即可完成移動構造的操作。

同樣的,我們還可以定義移動賦值運算。

值得注意的是,兩個移動函式都添加了noexcept識別符號。這也是C++11新標準中引入的,用於向標準庫指明此函式不會丟擲異常,以避免標準庫在和我們定義的這個類進行互動時做一些不必要的工作。如果我們不承諾noexcept,那麼當標準庫容器擴充套件容量時,就不能呼叫移動建構函式來移動容器內的現存元素,而只能採取比較耗費資源的拷貝建構函式。

這一部分的測試程式碼如下:

std::cout<<"test custom move copy constructor/move assign operator.\n";
IntVec iv1(10);
for(size_t i = 0; i < 5; i++)
    iv1.push_back(i);
std::cout<<"-------iv1:\n";
iv1.print_info();

IntVec iv2(std::move(iv1));
std::cout<<"-------iv2:\n";
iv2.print_info();
std::cout<<"-------iv1:\n";
iv1.print_info();

IntVec iv3 = iv2;
std::cout<<"-------iv3:\n";
iv3.print_info();
std::cout<<"-------iv2:\n";
iv2.print_info();

IntVec iv4(5);
std::cout<<"-------iv4:\n";
iv4.print_info();
iv4 = std::move(iv2);
std::cout<<"-------iv4:\n";
iv4.print_info();
std::cout<<"-------iv2:\n";
iv2.print_info();

std::cout<<"test custom move copy constructor/move assign operator done.\n"<<std::endl;

移動迭代器

新標準中提供了std::make_move_iterator函式用於從普通迭代器獲得移動迭代器。對移動迭代器解引用將會獲得對應的右值引用,從而方便的對整個容器進行移動操作。

std::cout<<"test move iterator:\n";
auto new_strs2 = alloc.allocate(size);
std::uninitialized_copy(std::make_move_iterator(new_strs),
        std::make_move_iterator(new_strs + size),
        new_strs2);
std::cout<<"new_strs[0]: "<<new_strs[0]<<std::endl;
std::cout<<"new_strs2[0]: "<<new_strs2[0]<<std::endl;
for(size_t i = 0; i < size; i++)
{
    alloc.destroy(new_strs + i);
}
alloc.deallocate(new_strs, size);
std::cout<<"test move iterator done.\n"<<std::endl;

引用摺疊規則

當左右引用遇到模板引數的時候,需要用到引用摺疊規則來獲得最終的模板推斷型別和形參型別。

template <typename T>
void vague_func(T&& val)
{
    std::cout<<"val: "<<val<<std::endl;
    T val2 = val;
    val2++;
    std::cout<<"val2: "<<val2<<'\t'<<"val: "<<val<<std::endl;
}

std::cout<<"test ref folding:\n";
int val = 2;
int &lref = val;
int &&rref = 2;
std::cout<<"-------with val:\n";
vague_func(2);
std::cout<<"-------with lref:\n";
vague_func(lref);
std::cout<<"-------with rref:\n";
vague_func(rref);
vague_func(std::move(val));
std::cout<<"test ref done.\n"<<std::endl;

在上述vague_func中,雖然val的型別是T&&,看上去是個右值引用,但是實際上也是可以接受左值引用的型別的。當傳入一個左值時,如lref,編譯器會推斷T = int&而不是T = int。那麼這時實際例項化的vague_func實際是:

void vague_func(int& && val)

根據引用摺疊規則,除了T&& &&摺疊為T&&之外的所有情況均摺疊為T&,那麼最終vague_func為:

void vague_func(int& val)

因此,vague_func也可以接受一個左值實參。這種引用摺疊規則,也是std::move得以實現的基礎,有興趣的讀者可以自行去了解下其實現,就一行程式碼^o^

但是,vague_func的模板型別推斷規則,也造成了T型別的不確定(int還是int&?),這給後續的編碼也帶來了困難。

std::forward

在上述vague_func中,如果傳入一個右值,但是val卻是一個變數,也就是一個左值。那麼如何保持原來實參的型別資訊呢,這時需要用到std::forward。

std::forward(val)返回型別是T&&,這時,根據摺疊規則,如果實參val是個左值,則返回T&;如果是右值,則返回T&&。

void f(int &&i)
{
    std::cout<<i<<"\t i is a right ref.\n";
}

void g(int &i)
{
    std::cout<<i<<"\t i is a left ref.\n";
}

template <typename F, typename T>
void forward_func(F f, T&& val)
{
    f(std::forward<T>(val));
}

std::cout<<"test forward:\n";
forward_func(f, 5);
forward_func(g, rref);
forward_func(g, val);
std::cout<<"test forward done.\n"<<std::endl;

所有程式的輸出:

test move constructor:
old_strs[0]: abcde
new_strs[0]: abcde
old_strs[0]: 
test move constructor done.

test rvalue reference:
42      42      1764    1764    1764
test rvalue ref done.

test std::move:
bzdf    bzdf    bzdf
test std::move done.

test custom move copy constructor/move assign operator.
-------iv1:
capacity: 10
size: 5
pointer: 0x1523160
IntVect move constructor.
-------iv2:
capacity: 10
size: 5
pointer: 0x1523160
-------iv1:
capacity: 0
size: 0
pointer: nullptr
IntVect copy constructor.
-------iv3:
capacity: 10
size: 5
pointer: 0x1523190
-------iv2:
capacity: 10
size: 5
pointer: 0x1523160
-------iv4:
capacity: 5
size: 0
pointer: 0x15231c0
IntVect move assign constructor.
-------iv4:
capacity: 10
size: 5
pointer: 0x1523160
-------iv2:
capacity: 0
size: 0
pointer: nullptr
test custom move copy constructor/move assign operator done.

test move iterator:
new_strs[0]: 
new_strs2[0]: abcde
test move iterator done.

test ref folding:
-------with val:
val: 2
val2: 3 val: 2
-------with lref:
val: 2
val2: 3 val: 3
-------with rref:
val: 2
val2: 3 val: 3
val: 3
val2: 4 val: 3
test ref done.

test forward:
5        i is a right ref.
3        i is a left ref.
3        i is a left ref.
test forward done.

總結

  • 新標準中允許通過&&標識定義一個右值引用,將其繫結到一個右值上。
  • std::move函式的作用是獲得一個變數的右值引用。
  • 移動構造,就是接受一個右值引用,從而接受(竊取)該右值引用所引用的物件,而沒有實際的大塊記憶體拷貝操作,並且保證被竊取後的物件可析構可銷燬。
  • 可以定義自己的移動建構函式以及移動賦值運算。
  • noexcept用於向標準庫指明此函式不會丟擲異常。宣告移動建構函式和移動賦值運算為noexcept以避免標準庫在和我們定義的這個類進行互動時做一些不必要的工作。
  • 新標準中提供了std::make_move_iterator函式用於從普通迭代器獲得移動迭代器。對移動迭代器解引用將會獲得對應的右值引用,從而方便的對整個容器進行移動操作。
  • 引用摺疊規則,除了T&& &&摺疊為T&&之外的所有情況均摺疊為T&,主要用於模板型別推斷中。
  • std::forward(val)用於保持實參的左右值資訊。