c++ - tutorial - 什麼是移動語義?




rvalue reference (8)

Move semantics is about transferring resources rather than copying them when nobody needs the source value anymore.

In C++03, objects are often copied, only to be destroyed or assigned-over before any code uses the value again. For example, when you return by value from a function—unless RVO kicks in—the value you're returning is copied to the caller's stack frame, and then it goes out of scope and is destroyed. This is just one of many examples: see pass-by-value when the source object is a temporary, algorithms like sort that just rearrange items, reallocation in vector when its capacity() is exceeded, etc.

When such copy/destroy pairs are expensive, it's typically because the object owns some heavyweight resource. For example, vector<string> may own a dynamically-allocated memory block containing an array of string objects, each with its own dynamic memory. Copying such an object is costly: you have to allocate new memory for each dynamically-allocated blocks in the source, and copy all the values across. Then you need deallocate all that memory you just copied. However, moving a large vector<string> means just copying a few pointers (that refer to the dynamic memory block) to the destination and zeroing them out in the source.

我剛剛聽完了關於C++0x關於Scott Meyers的軟件工程無線電播客採訪 。 大部分新功能對我來說都很有意義,而且我現在對C ++ 0x感到非常興奮,除了一個。 我仍然沒有得到移動語義 ...他們究竟是什麼?


I'm writing this to make sure I understand it properly.

Move semantics were created to avoid the unnecessary copying of large objects. Bjarne Stroustrup in his book "The C++ Programming Language" uses two examples where unnecessary copying occurs by default: one, the swapping of two large objects, and two, the returning of a large object from a method.

Swapping two large objects usually involves copying the first object to a temporary object, copying the second object to the first object, and copying the temporary object to the second object. For a built-in type, this is very fast, but for large objects these three copies could take a large amount of time. A "move assignment" allows the programmer to override the default copy behavior and instead swap references to the objects, which means that there is no copying at all and the swap operation is much faster. The move assignment can be invoked by calling the std::move() method.

Returning an object from a method by default involves making a copy of the local object and its associated data in a location which is accessible to the caller (because the local object is not accessible to the caller and disappears when the method finishes). When a built-in type is being returned, this operation is very fast, but if a large object is being returned, this could take a long time. The move constructor allows the programmer to override this default behavior and instead "reuse" the heap data associated with the local object by pointing the object being returned to the caller to heap data associated with the local object. Thus no copying is required.

在不允許創建本地對象(即堆棧中的對象)的語言中,不會出現這些類型的問題,因為所有對像都分配在堆上,並且始終通過引用進行訪問。


In easy (practical) terms:

Copying an object means copying its "static" members and calling the new operator for its dynamic objects. 對?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

However, to move an object (I repeat, in a practical point of view) implies only to copy the pointers of dynamic objects, and not to create new ones.

But, is that not dangerous? Of course, you could destruct a dynamic object twice (segmentation fault). So, to avoid that, you should "invalidate" the source pointers to avoid destructing them twice:

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

Ok, but if I move an object, the source object becomes useless, no? Of course, but in certain situations that's very useful. The most evident one is when I call a function with an anonymous object (temporal, rvalue object, ..., you can call it with different names):

void heavyFunction(HeavyType());

In that situation, an anonymous object is created, next copied to the function parameter, and afterwards deleted. So, here it is better to move the object, because you don't need the anonymous object and you can save time and memory.

This leads to the concept of an "rvalue" reference. They exist in C++11 only to detect if the received object is anonymous or not. I think you do already know that an "lvalue" is an assignable entity (the left part of the = operator), so you need a named reference to an object to be capable to act as an lvalue. A rvalue is exactly the opposite, an object with no named references. Because of that, anonymous object and rvalue are synonyms. 所以:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

In this case, when an object of type A should be "copied", the compiler creates a lvalue reference or a rvalue reference according to if the passed object is named or not. When not, your move-constructor is called and you know the object is temporal and you can move its dynamic objects instead of copying them, saving space and memory.

It is important to remember that "static" objects are always copied. There's no ways to "move" a static object (object in stack and not on heap). So, the distinction "move"/ "copy" when an object has no dynamic members (directly or indirectly) is irrelevant.

If your object is complex and the destructor has other secondary effects, like calling to a library's function, calling to other global functions or whatever it is, perhaps is better to signal a movement with a flag:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

So, your code is shorter (you don't need to do a nullptr assignment for each dynamic member) and more general.

Other typical question: what is the difference between A&& and const A&& ? Of course, in the first case, you can modify the object and in the second not, but, practical meaning? In the second case, you can't modify it, so you have no ways to invalidate the object (except with a mutable flag or something like that), and there is no practical difference to a copy constructor.

And what is perfect forwarding ? It is important to know that a "rvalue reference" is a reference to a named object in the "caller's scope". But in the actual scope, a rvalue reference is a name to an object, so, it acts as a named object. If you pass an rvalue reference to another function, you are passing a named object, so, the object isn't received like a temporal object.

void some_function(A&& a)
{
   other_function(a);
}

The object a would be copied to the actual parameter of other_function . If you want the object a continues being treated as a temporary object, you should use the std::move function:

other_function(std::move(a));

With this line, std::move will cast a to an rvalue and other_function will receive the object as a unnamed object. Of course, if other_function has not specific overloading to work with unnamed objects, this distinction is not important.

Is that perfect forwarding? Not, but we are very close. Perfect forwarding is only useful to work with templates, with the purpose to say: if I need to pass an object to another function, I need that if I receive a named object, the object is passed as a named object, and when not, I want to pass it like a unnamed object:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

That's the signature of a prototypical function that uses perfect forwarding, implemented in C++11 by means of std::forward . This function exploits some rules of template instantiation:

 `A& && == A&`
 `A&& && == A&&`

So, if T is a lvalue reference to A ( T = A&), a also ( A& && => A&). If T is a rvalue reference to A , a also (A&& && => A&&). In both cases, a is a named object in the actual scope, but T contains the information of its "reference type" from the caller scope's point of view. This information ( T ) is passed as template parameter to forward and 'a' is moved or not according to the type of T .


It's like copy semantics, but instead of having to duplicate all of the data you get to steal the data from the object being "moved" from.


You know what a copy semantics means right? it means you have types which are copyable, for user-defined types you define this either buy explicitly writing a copy constructor & assignment operator or the compiler generates them implicitly. This will do a copy.

Move semantics is basically a user-defined type with constructor that takes an r-value reference (new type of reference using && (yes two ampersands)) which is non-const, this is called a move constructor, same goes for assignment operator. So what does a move constructor do, well instead of copying memory from it's source argument it 'moves' memory from the source to the destination.

When would you want to do that? well std::vector is an example, say you created a temporary std::vector and you return it from a function say:

std::vector<foo> get_foos();

You're going to have overhead from the copy constructor when the function returns, if (and it will in C++0x) std::vector has a move constructor instead of copying it can just set it's pointers and 'move' dynamically allocated memory to the new instance. It's kind of like transfer-of-ownership semantics with std::auto_ptr.


假設你有一個返回實質對象的函數:

Matrix multiply(const Matrix &a, const Matrix &b);

當你編寫這樣的代碼時:

Matrix r = multiply(a, b);

那麼普通的C ++編譯器會為multiply()的結果創建一個臨時對象,調用copy構造函數初始化r ,然後破壞臨時返回值。 在C ++ 0x中移動語義允許調用“移動構造函數”通過複製其內容來初始化r ,然後丟棄臨時值而不必破壞它。

如果(比如上面的Matrix示例)這個特別重要,被複製的對象會在堆上分配額外的內存來存儲其內部表示。 複製構造函數將不得不製作內部表示的完整副本,或者間接使用引用計數和寫入時復制語義。 移動構造函數將單獨留下堆內存,並將指針複製到Matrix對象內。


我的第一個回答是移動語義的一個非常簡單的介紹,並且許多細節被保留下來以保持簡單。 然而,移動語義還有很多,我認為是第二次填補空白的時候了。 第一個答案已經很老了,而且把它換成完全不同的文本並不合適。 我認為它作為第一次介紹仍然很好。 但如果你想深入挖掘,請閱讀:)

Stephan T. Lavavej花時間提供了寶貴的反饋意見。 謝謝,斯蒂芬!

介紹

移動語義允許對像在某些條件下獲得其他對象的外部資源的所有權。 這在兩個方面很重要:

  1. 把昂貴的拷貝變成便宜的動作。 看到我的第一個答案為例。 請注意,如果一個對像不管理至少一個外部資源(直接或通過它的成員對象間接管理),移動語義不會提供超過複製語義的任何優勢。 在這種情況下,複製對象和移動對象意味著完全相同的事情:

    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
    
  2. 實施安全的“僅移動”類型; 也就是說,複製沒有意義的類型,但移動確實如此。 示例包括鎖,文件句柄和具有唯一所有權語義的智能指針。 注意:這個答案討論了std::auto_ptr ,這是一個過時的C ++ 98標準庫模板,它在C ++ 11中被替換為std::unique_ptr 。 中級C ++程序員可能至少對std::auto_ptr有些熟悉,並且由於顯示了“移動語義”,它似乎是討論C ++ 11中的移動語義的一個很好的起點。 因人而異。

什麼是舉動?

C ++ 98標準庫提供了一個具有唯一所有權語義的智能指針,稱為std::auto_ptr<T> 。 如果你不熟悉auto_ptr ,它的目的是保證一個動態分配的對象總是被釋放,即使在例外的情況下:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

關於auto_ptr的不尋常的事情是它的“複製”行為:

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

注意如何用a初始化b 不會復制三角形,而是將三角形的所有權從a轉移到b 。 我們也說“ a移到 b ”或“三角從a 移到 b ”。 這可能聽起來很混亂,因為三角形本身總是停留在內存中的相同位置。

移動對象意味著將其管理的某些資源的所有權轉移給另一個對象。

auto_ptr的拷貝構造函數可能看起來像這樣(有些簡化):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

危險和無害的舉動

關於auto_ptr的危險之處在於語法上看起來像副本的內容實際上是一種移動。 嘗試在移出的auto_ptr上調用成員函數將會調用未定義的行為,因此必須非常小心,在將它從以下位置移出後不要使用auto_ptr

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

auto_ptr並不總是危險的。 工廠函數對於auto_ptr是一個非常好的用例:

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

請注意兩個示例如何遵循相同的語法模式:

auto_ptr<Shape> variable(expression);
double area = expression->area();

然而,其中一個調用未定義的行為,而另一個則不行。 那麼表達式amake_triangle()之間有什麼區別? 他們不是同一類型嗎? 事實上他們是,但他們有不同的價值類別

價值類別

顯然,表達式a中的auto_ptr變量和表達式make_triangle()之間必定存在某種深刻的區別,它表示調用一個函數的調用,該函數按值返回一個auto_ptr ,因此每次調用它時都會創建一個新的臨時auto_ptr對象。 a是一個左值的例子,而make_triangle()是一個右值的例子。

從諸如a左值移動是很危險的,因為我們稍後可以嘗試通過調用未定義的行為來調用成員函數。 另一方面,從諸如make_triangle()類的make_triangle()移動是完全安全的,因為在復制構造函數完成其工作之後,我們不能再次使用臨時文件。 沒有表示表示暫時的; 如果我們再次寫make_triangle() ,我們會得到一個不同的臨時文件。 事實上,從下一行開始移動的臨時文件已經消失:

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

請注意,字母lr在作業的左側和右側具有歷史淵源。 這在C ++中不再是真實的,因為有左值不能出現在賦值的左側(比如數組或者沒有賦值運算符的用戶定義類型),並且有一些右值(類型的所有右值與一個賦值操作符)。

類類型的右值是一個表達式,其評估創建一個臨時對象。 在正常情況下,同一範圍內的其他表達式不會表示相同的臨時對象。

右值引用

我們現在明白從左值移動是有潛在危險的,但從右值移動是無害的。 如果C ++有語言支持來區分左值參數和右值參數,我們可以完全禁止從左值移動,或者至少在顯式調用時從左值移動,這樣我們就不會意外移動了。

C ++ 11對這個問題的答案是右值引用 。 右值引用是一種新的引用,它只綁定到右值,語法是X&& 。 好的舊參考X&現在被稱為左值參考 。 (請注意, X&& 不是對引用的引用;在C ++中沒有這種東西。)

如果我們將const放入混合中,我們已經有了四種不同的引用。 X可以綁定什麼類型的表達式?

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes

在實踐中,你可以忘記const X&& 。 限制讀取rvalues並不是很有用。

右值引用X&&是一種新的引用,只能綁定到右值。

隱式轉換

右值引用經歷了幾個版本。 從版本2.1開始,右值引用X&&也綁定到不同類型Y所有值類別,前提是存在從YX的隱式轉換。 在這種情況下,會創建X類型的臨時值,並將右值引用綁定到該臨時值:

void some_function(std::string&& r);

some_function("hello world");

在上面的例子中, "hello world"是一個類型為const char[12]的右值。 由於存在從const char[12]const char*std::string的隱式轉換,因此會創建一個類型為std::string的臨時表,並將r綁定到該臨時表。 這是rvalues(表達式)和臨時對象(對象)之間的區別有點模糊的情況之一。

移動構造函數

具有X&&參數的函數的一個有用示例是移動構造函數 X::X(X&& source) 。 其目的是將受管資源的所有權從源移交給當前對象。

在C ++ 11中, std::auto_ptr<T>已被std::unique_ptr<T>所取代,它利用右值引用。 我將開發並討論unique_ptr的簡化版本。 首先,我們封裝一個原始指針並重載運算符->* ,這樣我們的類就像一個指針:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

構造函數接受對象的所有權,並且析構函數將其刪除:

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

現在來了有趣的部分,移動構造函數:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

這個移動構造函數完全做到了auto_ptr拷貝構造函數所做的,但它只能用rvalues提供:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

第二行無法編譯,因為a是一個左值,但參數unique_ptr&& source只能綁定到右值。 這正是我們想要的; 危險的舉動絕不應該隱含。 第三行編譯得很好,因為make_triangle()是一個右值。 移動構造函數將把所有權從臨時轉移到c 。 再一次,這正是我們想要的。

移動構造函數將託管資源的所有權轉移到當前對像中。

移動賦值運算符

最後一個缺失的部分是移動賦值操作符。 它的工作是釋放舊資源並從其論點中獲得新資源:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

請注意移動賦值運算符的這種實現如何復制析構函數和移動構造函數的邏輯。 你是否熟悉複製交換習慣用法? 它也可以用於移動語義作為移動和交換的習慣用法:

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

現在,該sourceunique_ptr類型的變量,它將由移動構造函數初始化; 也就是說,參數將被移入參數中。 該參數仍然需要是右值,因為移動構造函數本身俱有右值引用參數。 當控制流達到operator=的右大括號時, source超出範圍,自動釋放舊的資源。

移動賦值操作符將託管資源的所有權轉移到當前對像中,釋放舊資源。 移動和交換習慣用法簡化了實現。

從左值移動

有時候,我們想從左值移動。 也就是說,有時我們希望編譯器將左值視為右值,因此它可以調用移動構造函數,即使它可能不安全。 為此,C ++ 11在頭文件<utility>提供了一個名為std::move的標準庫函數模板。 這個名字有點不幸,因為std::move只是將左值轉換為右值; 它本身不會移動任何東西。 它只是使移動。 也許它應該被命名為std::cast_to_rvalue或者std::enable_move ,但是現在我們被std::cast_to_rvalue了這個名字。

以下是你如何從一個左值顯式移動:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

請注意,在第三行之後,不再擁有一個三角形。 沒關係,因為通過明確地std::move(a) ,我們明確地表達了我們的意圖:“親愛的構造函數,為了初始化c做任何你想要的;我不再關心a了。用你的方式。“

std::move(some_lvalue)將左值轉換為右值,從而啟用後續移動。

Xvalues

請注意,即使std::move(a)是一個右值,它的評估也不會創建一個臨時對象。 這個難題迫使委員會引入第三個價值類別。 可以綁定到右值引用的東西,即使它不是傳統意義上的右值,也稱為xvalue (eXpiring值)。 傳統的rvalues被重新命名為prvalues (純rvalues)。

prvalues和xvalues都是rvalues。 Xvalues和Lvalues都是glvalues (廣義左值 )。 用關係圖更容易理解關係:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

請注意,只有xvalues是新的; 剩下的只是由於重命名和分組。

C ++ 98 rvalues在C ++ 11中被稱為prvalues。 在前面的段落中將所有出現的“右值”用“prvalue”精神代替。

走出功能

到目前為止,我們已經看到移動到局部變量和功能參數中。 但移動也可能在相反的方向。 如果一個函數按值返回,那麼在調用站點(可能是一個局部變量或一個臨時的對象,但可以是任何類型的對象)上的某個對象將被作為移動構造函數的參數的return語句之後的表達式初始化:

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

也許令人驚訝的是,自動對象(未聲明為static局部變量)也可以隱式地移出函數:

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

移動構造函數如何接受左值result作為參數? result範圍即將結束,並且在堆棧展開期間將被破壞。 事後沒有人可能會抱怨result已經發生了變化; 當控制流返回到調用者時, result不再存在! 因此,C ++ 11有一個特殊的規則,允許從函數返回自動對象,而不必寫std::move 。 事實上,你絕對不應該使用std::move將自動對象移出函數,因為這會禁止“命名返回值優化”(NRVO)。

切勿使用std::move將自動對象移出函數。

請注意,在兩個工廠函數中,返回類型是一個值,而不是右值引用。 右值引用仍然是引用,並且一如既往,您不應該返回對自動對象的引用; 如果你欺騙編譯器接受你的代碼,調用者最終會得到一個懸而未決的引用,如下所示:

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

切勿通過右值引用返回自動對象。 移動僅由移動構造函數執行,而不是由std::move ,而不是僅通過將右值​​綁定到右值引用。

進入成員

遲早你會寫這樣的代碼:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

基本上,編譯器會抱怨parameter是一個左值。 如果你看看它的類型,你會看到一個右值引用,但右值引用僅僅意味著“一個綁定到右值的引用”。 這並不意味著參考本身是一個右值! 事實上, parameter只是一個具有名稱的普通變量。 您可以在構造函數的主體內經常使用parameter ,並且它始終表示同一個對象。 隱含地從它移動將是危險的,因此語言禁止它。

一個命名的右值引用是一個左值,就像任何其他變量一樣。

解決方案是手動啟用移動:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

您可能會爭辯說, parametermember初始化後不再使用。 為什麼沒有像返回值一樣靜靜插入std::move特殊規則? 可能是因為編譯器實現者負擔過重。 例如,如果構造函數體在另一個翻譯單元中呢? 相比之下,返回值規則只需檢查符號表以確定return關鍵字之後的標識符是否表示自動對象。

您也可以按值傳遞parameter 。 對於像unique_ptr這樣的移動類型,似乎還沒有成熟的習慣用法。 就我個人而言,我更喜歡按價值傳遞,因為它會減少界面中的混亂。

特殊會員功能

C ++ 98根據需要隱式聲明三個特殊成員函數,即當它們在某處需要時:複製構造函數,複製賦值運算符和析構函數。

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

右值引用經歷了幾個版本。 從3.0版本開始,C ++ 11根據需要聲明兩個額外的特殊成員函數:移動構造函數和移動賦值運算符。 請注意,VC10和VC11都不符合版本3.0,因此您必須自行實施它們。

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

如果沒有任何特殊成員函數是手動聲明的,則這兩個新的特殊成員函數只會隱式聲明。 此外,如果您聲明了自己的移動構造函數或移動賦值運算符,則復制構造函數和復制賦值運算符都不會隱式聲明。

這些規則在實踐中意味著什麼?

如果你編寫一個沒有非託管資源的類,就不需要自己聲明任何五個特殊成員函數,並且你將得到正確的複制語義並且免費移動語義。 否則,你將不得不自己實現特殊的成員函數。 當然,如果你的類沒有從移動語義中獲益,就不需要實現特殊移動操作。

請注意,可以將復制賦值運算符和移動賦值運算符合併為一個統一的賦值運算符,並按值賦值:

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

這樣一來,實現特殊成員函數的數量從五個減少到四個。 在這裡異常安全和效率之間有一個權衡,但我不是這個問題的專家。

轉發引用( previously稱為通用引用

考慮下面的函數模板:

template<typename T>
void foo(T&&);

您可能會希望T&&只綁定到右值,因為乍一看,它看起來像右值引用。 事實證明, T&&也綁定到左值:

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

如果參數是類型X的右值,則T被推斷為X ,因此T&&意味著X&& 。 這是任何人都會期待的。 但是,如果參數是X類型的左值,由於特殊規則, T被推斷為X& ,因此T&&意味著類似於X& && 。 但是由於C ++仍然沒有引用引用的概念,所以X& &&類型被折疊X& 。 這聽起來可能會讓人感到困惑和無用,但參考折疊對於完美轉發來說是必不可少的(這裡不再討論)。

T &&不是右值引用,而是轉發引用。 它也綁定到左值,在這種情況下, TT&&都是左值引用。

如果你想限制一個函數模板為右值,你可以將SFINAE和類型特徵結合起來:

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

搬遷的實施

現在你明白引用崩潰了,下面是如何實現std::move

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

正如你所看到的, move通過轉發引用T&&接受任何類型的參數,並且它返回一個右值引用。 std::remove_reference<T>::type元函數調用是必須的,因為否則,對於X類型的左值,返回類型將是X& && ,它會折疊為X& 。 由於t總是一個左值(記住一個名為右值的引用是一個左值),但我們希望將t綁定到右值引用,所以我們必須明確地將t賦給正確的返回類型。 一個返回右值引用的函數本身就是一個xvalue。 現在你知道xvalues來自哪裡;)

調用返回右值引用的函數(如std::move )是一個xvalue。

請注意,在此示例中,通過右值引用返回很好,因為t不表示自動對象,而是由調用者傳入的對象。


移動語義基於右值引用
右值是一個臨時對象,它將在表達式的末尾被銷毀。 在當前的C ++中,右值只能綁定到const引用。 C ++ 1x將允許非常量右值引用,拼寫T&& ,它們是對右值對象的引用。
由於右值將在表達式的末尾死亡,因此可以竊取其數據 。 不是複製到另一個對像中,而是其數據移入其中。

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

在上面的代碼中,對於舊編譯器,使用X的拷貝構造函數將f()的結果復製x 。 如果你的編譯器支持移動語義,並且X有一個移動構造函數,那就調用它。 由於它的rhs論證是一個右值 ,我們知道它不再需要,我們可以竊取它的價值。
因此,該值將從f()返回的未命名臨時文件移動x (而x的數據,初始化為空X ,移動到臨時文件中,這會在分配後被銷毀)。







move-semantics