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

rrix8589 8年前發布 | 31K 次閱讀 C/C++開發 C/C++

前六篇在這里:

C++11新特性之新類型與初始化: http://blog.guoyb.com/2016/06/18/cpp11-1

C++11新特性之類型推斷與類型獲取: http://blog.guoyb.com/2016/06/25/cpp11-2

C++11新特性之lambda: http://blog.guoyb.com/2016/06/30/cpp11-3

C++11新特性之容器相關特性: http://blog.guoyb.com/2016/07/09/cpp11-4

C++11新特性之智能指針: http://blog.guoyb.com/2016/08/02/cpp11-5

C++11新特性之Class: http://blog.guoyb.com/2016/08/14/cpp11-6

這是C++11新特性介紹的第七部分,涉及到左右值引用、移動構造、移動賦值、完美轉發等。

不想看toy code的讀者可以直接拉到文章最后看這部分的總結。

右值引用

右值是一個行將銷毀的值,例如(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.

總結

  1. 新標準中允許通過&&標識定義一個右值引用,將其綁定到一個右值上。
  2. std::move函數的作用是獲得一個變量的右值引用。
  3. 移動構造,就是接受一個右值引用,從而接受(竊取)該右值引用所引用的對象,而沒有實際的大塊內存拷貝操作,并且保證被竊取后的對象可析構可銷毀。
  4. 可以定義自己的移動構造函數以及移動賦值運算。
  5. noexcept用于向標準庫指明此函數不會拋出異常。聲明移動構造函數和移動賦值運算為noexcept以避免標準庫在和我們定義的這個類進行交互時做一些不必要的工作。
  6. 新標準中提供了std::make_move_iterator函數用于從普通迭代器獲得移動迭代器。對移動迭代器解引用將會獲得對應的右值引用,從而方便的對整個容器進行移動操作。
  7. 引用折疊規則,除了T&& &&折疊為T&&之外的所有情況均折疊為T&,主要用于模板類型推斷中。
  8. std::forward (val)用于保持實參的左右值信息。

完整代碼詳見 move_and_forward.cpp

轉載請注明出處: http://blog.guoyb.com/2016/08/20/cpp11-7/

歡迎使用微信掃描下方二維碼,關注我的微信公眾號TechTalking,技術·生活·思考:

 

來自:http://blog.guoyb.com/2016/08/20/cpp11-7/

 

 本文由用戶 rrix8589 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!