【翻譯】VC10中的C++0x新特性:右值引用(rvalue references) (2)

來源:互聯網
上載者:User

 

零度の冰翻譯,原文地址在此,轉載請註明出處。

接【翻譯】VC10中的C++0x新特性:右值引用(rvalue references) (1)

move語義:移動左值

現在,如果你喜歡使用賦值運算子來實現拷貝建構函式的話,將會怎麼樣?你可能會嘗試使用你的move賦值運算子去實現move建構函式。這是可能的,但是你需要小心,下面就是一種錯誤的做法:

C:/Temp>type unified_wrong.cpp
#include <stddef.h>
#include <iostream>
#include <ostream>
using namespace std;

class remote_integer {
public:
    remote_integer() {
        cout << "Default constructor." << endl;

        m_p = NULL;
    }

    explicit remote_integer(const int n) {
        cout << "Unary constructor." << endl;

        m_p = new int(n);
    }

    remote_integer(const remote_integer& other) {
        cout << "Copy constructor." << endl;

        m_p = NULL;
        *this = other;
    }

#ifdef MOVABLE
    remote_integer(remote_integer&& other) {
        cout << "MOVE CONSTRUCTOR." << endl;

        m_p = NULL;
        *this = other; // WRONG
    }
#endif // #ifdef MOVABLE

    remote_integer& operator=(const remote_integer& other) {
        cout << "Copy assignment operator." << endl;

        if (this != &other) {
            delete m_p;

            if (other.m_p) {
                m_p = new int(*other.m_p);
            } else {
                m_p = NULL;
            }
        }

        return *this;
    }

#ifdef MOVABLE
    remote_integer& operator=(remote_integer&& other) {
        cout << "MOVE ASSIGNMENT OPERATOR." << endl;

        if (this != &other) {
            delete m_p;

            m_p = other.m_p;
            other.m_p = NULL;
        }

        return *this;
    }
#endif // #ifdef MOVABLE

    ~remote_integer() {
        cout << "Destructor." << endl;

        delete m_p;
    }

    int get() const {
        return m_p ? *m_p : 0;
    }

private:
    int * m_p;
};

remote_integer frumple(const int n) {
    if (n == 1729) {
        return remote_integer(1729);
    }

    remote_integer ret(n * n);

    return ret;
}

int main() {
    remote_integer x = frumple(5);

    cout << x.get() << endl;

    remote_integer y = frumple(1729);

    cout << y.get() << endl;
}

C:/Temp>cl /EHsc /nologo /W4 /O2 unified_wrong.cpp
unified_wrong.cpp

C:/Temp>unified_wrong
Unary constructor.
Copy constructor.
Copy assignment operator.
Destructor.
25
Unary constructor.
1729
Destructor.
Destructor.

C:/Temp>cl /EHsc /nologo /W4 /O2 /DMOVABLE unified_wrong.cpp
unified_wrong.cpp

C:/Temp>unified_wrong
Unary constructor.
MOVE CONSTRUCTOR.
Copy assignment operator.
Destructor.
25
Unary constructor.
1729
Destructor.
Destructor.

(編譯器在這裡實施了RVO最佳化,但沒有NRVO。我上面已經提到,有些拷貝建構函式的調用被RVO和NRVO最佳化掉了,但編譯器不是每次都能實施這種最佳化,move建構函式最佳化掉了剩餘的情況)

在move建構函式裡標記WRONG的那一行調用了copy賦值運算子,而不是move賦值運算子!這樣也能正常編譯運行,但move建構函式的目的沒有達到。

為啥會這樣呢?從C++98/03裡回想一下,具名的左值引用是左值(int& r = *p;,r是左值)匿名的左值引用還是左值(vector<int>v(10, 1729),調用v[0]返回int&,這是個匿名左值引用,它的地址是可以擷取的)。右值引用的行為就不同了:

  • 具名的右值引用是左值。
  • 匿名的右值引用是右值。

具名的左值引用可以被重複的使用,也可以在上面施加多次操作。如果讓具名左值引用成為右值的話,那施加在其身上的第一個操作可能就會把它的資源偷走,導致後續的操作失效。偷取是不應該影響其它操作的,因此具名左值引用應該是左值。另一方面,匿名的右值引用不會被重複使用,因此它可以保持自己的右值屬性。

如果你真的想用move賦值運算子實現move建構函式,你需要一種把左值看作是右值的能力。C++0x標頭檔<utility>中的std::move()賦予你了這種能力,VC10會包含它。(譯註:原文寫作時,std::move()尚未包含進VC10,因此作者接著給出了std::move的實現)

C:/Temp>type unified_right.cpp
#include <stddef.h>
#include <iostream>
#include <ostream>
using namespace std;

template <typename T> struct RemoveReference {
     typedef T type;
};

template <typename T> struct RemoveReference<T&> {
     typedef T type;
};

template <typename T> struct RemoveReference<T&&> {
     typedef T type;
};

template <typename T> typename RemoveReference<T>::type&& Move(T&& t) {
    return t;
}

class remote_integer {
public:
    remote_integer() {
        cout << "Default constructor." << endl;

        m_p = NULL;
    }

    explicit remote_integer(const int n) {
        cout << "Unary constructor." << endl;

        m_p = new int(n);
    }

    remote_integer(const remote_integer& other) {
        cout << "Copy constructor." << endl;

        m_p = NULL;
        *this = other;
    }

#ifdef MOVABLE
    remote_integer(remote_integer&& other) {
        cout << "MOVE CONSTRUCTOR." << endl;

        m_p = NULL;
        *this = Move(other); // RIGHT
    }
#endif // #ifdef MOVABLE

    remote_integer& operator=(const remote_integer& other) {
        cout << "Copy assignment operator." << endl;

        if (this != &other) {
            delete m_p;

            if (other.m_p) {
                m_p = new int(*other.m_p);
            } else {
                m_p = NULL;
            }
        }

        return *this;
    }

#ifdef MOVABLE
    remote_integer& operator=(remote_integer&& other) {
        cout << "MOVE ASSIGNMENT OPERATOR." << endl;

        if (this != &other) {
            delete m_p;

            m_p = other.m_p;
            other.m_p = NULL;
        }

        return *this;
    }
#endif // #ifdef MOVABLE

    ~remote_integer() {
        cout << "Destructor." << endl;

        delete m_p;
    }

    int get() const {
        return m_p ? *m_p : 0;
    }

private:
    int * m_p;
};

remote_integer frumple(const int n) {
    if (n == 1729) {
        return remote_integer(1729);
    }

    remote_integer ret(n * n);

    return ret;
}

int main() {
    remote_integer x = frumple(5);

    cout << x.get() << endl;

    remote_integer y = frumple(1729);

    cout << y.get() << endl;
}

C:/Temp>cl /EHsc /nologo /W4 /O2 /DMOVABLE unified_right.cpp
unified_right.cpp

C:/Temp>unified_right
Unary constructor.
MOVE CONSTRUCTOR.
MOVE ASSIGNMENT OPERATOR.
Destructor.
25
Unary constructor.
1729
Destructor.
Destructor.

(我將會把我實現的Move()作為std::move()來講,因為它們實現原理是一樣的)std::move()是如何工作的呢?目前,我只能告訴你這是個“牛×的魔法”。(一會兒我會詳細解釋,它並不複雜,但是它包括模版參數推導和引用退化(譯註:引用的引用 = 引用),後面講完美轉寄的時候還會遇到這倆東西)我可以用一個具體的例子來略過講述這個魔法:給一個string類型的左值,就像上面代碼中的up,std::move(up)調用的是 string&& std::move(string&),它返回的是個匿名的右值引用,而匿名的右值引用是右值。給定一個像上面代碼中的strange()這樣的右值string類型,std::move(strange())調用的是 string&& std::move(string&&),又一次,傳回值是匿名右值引用,還是右值。

std::move在其它地方也非常有用。任何時候只要你擁有一個左值,而你已經不再需要它了(它將要被銷毀或者被賦予別的值),你就可以使用std::move(左值運算式)來啟用move語義。

move語義:可移動的資料成員

C++0x的標準類(vector、string、regex等)都有move建構函式和move賦值運算子,並且我們已經瞭解了如何在自己的類中實現它們去手動的管理資源。但是當我們的類中有可move的資料成員(vector、string、regex)時咋弄?編譯器不會為我們自動產生move建構函式和move賦值運算子。因此我們需要自己實現它們。幸運的是,有了std::move(),這就非常簡單了:

C:/Temp>type point.cpp
#include <stddef.h>
#include <iostream>
#include <ostream>
using namespace std;

template <typename T> struct RemoveReference {
     typedef T type;
};

template <typename T> struct RemoveReference<T&> {
     typedef T type;
};

template <typename T> struct RemoveReference<T&&> {
     typedef T type;
};

template <typename T> typename RemoveReference<T>::type&& Move(T&& t) {
    return t;
}

class remote_integer {
public:
    remote_integer() {
        cout << "Default constructor." << endl;

        m_p = NULL;
    }

    explicit remote_integer(const int n) {
        cout << "Unary constructor." << endl;

        m_p = new int(n);
    }

    remote_integer(const remote_integer& other) {
        cout << "Copy constructor." << endl;

        if (other.m_p) {
            m_p = new int(*other.m_p);
        } else {
            m_p = NULL;
        }
    }

    remote_integer(remote_integer&& other) {
        cout << "MOVE CONSTRUCTOR." << endl;

        m_p = other.m_p;
        other.m_p = NULL;
    }

    remote_integer& operator=(const remote_integer& other) {
        cout << "Copy assignment operator." << endl;

        if (this != &other) {
            delete m_p;

            if (other.m_p) {
                m_p = new int(*other.m_p);
            } else {
                m_p = NULL;
            }
        }

        return *this;
    }

    remote_integer& operator=(remote_integer&& other) {
        cout << "MOVE ASSIGNMENT OPERATOR." << endl;

        if (this != &other) {
            delete m_p;

            m_p = other.m_p;
            other.m_p = NULL;
        }

        return *this;
    }

    ~remote_integer() {
        cout << "Destructor." << endl;

        delete m_p;
    }

    int get() const {
        return m_p ? *m_p : 0;
    }

private:
    int * m_p;
};

class remote_point {
public:
    remote_point(const int x_arg, const int y_arg)
        : m_x(x_arg), m_y(y_arg) { }

    remote_point(remote_point&& other)
        : m_x(Move(other.m_x)),
          m_y(Move(other.m_y)) { }

    remote_point& operator=(remote_point&& other) {
        m_x = Move(other.m_x);
        m_y = Move(other.m_y);
        return *this;
    }

    int x() const { return m_x.get(); }
    int y() const { return m_y.get(); }

private:
    remote_integer m_x;
    remote_integer m_y;
};

remote_point five_by_five() {
    return remote_point(5, 5);
}

remote_point taxicab(const int n) {
    if (n == 0) {
        return remote_point(1, 1728);
    }

    remote_point ret(729, 1000);

    return ret;
}

int main() {
    remote_point p = taxicab(43112609);

    cout << "(" << p.x() << ", " << p.y() << ")" << endl;

    p = five_by_five();

    cout << "(" << p.x() << ", " << p.y() << ")" << endl;
}

C:/Temp>cl /EHsc /nologo /W4 /O2 point.cpp
point.cpp

C:/Temp>point
Unary constructor.
Unary constructor.
MOVE CONSTRUCTOR.
MOVE CONSTRUCTOR.

Destructor.
Destructor.
(729, 1000)
Unary constructor.
Unary constructor.
MOVE ASSIGNMENT OPERATOR.
MOVE ASSIGNMENT OPERATOR.
Destructor.
Destructor.
(5, 5)
Destructor.
Destructor.

就像你看到的,對逐個移動每個資料成員非常的繁瑣。注意remote_point的move賦值運算子沒有做自賦值檢查因為remote_integer已經做了。還需要注意,remote_point隱式聲明的拷貝建構函式、賦值運算子和解構函式都正確的完成了相應功能。

最後一個議題:你應該儘可能的為你的可拷貝的類實現move建構函式和move賦值運算子,因為編譯器不會幫你產生它們。這樣,不僅能在平時使用這些類時從move語義獲得好處,STL容器和通用演算法也可以得到move語義的好處,因為它們可以用移動代替昂貴的複製了。

轉寄的問題

C++98/03中關於左值、右值、引用和模板的規則看起來非常完美。直到程式員嘗試去寫一些高度泛化的代碼時,問題出現了。假設你要寫一個完全泛化的函數outer(),它的目的是擷取任意數目,任意類型的參數,然後將它們轉寄給函數inner()。已經有了一些不錯的解決方案,比如Factory 方法make_shared<T>(args)將args轉寄給T的建構函式,並且返回一個shared_ptr<T>。(這樣就把T類型的對象和它的引用計數儲存在了同一個記憶體塊中,效率上和侵入式引用計數一樣好)像function<Ret (Args)>這樣的封裝類,可以將參數傳遞給其內部儲存的函數對象,等等。在本篇文章中,我們只對outer()將參數轉寄給inner()感興趣。至於outer()的傳回值類型如何推定,那是另外一個問題。(有時候很簡單,比如make_shared<T>(args)總是返回shared_ptr<T>。但要完全解決這個問題,就需要用到C++0x的特性decltype了)

沒有參數的情況就不討論了,當參數為一個的情況時,讓我們嘗試去寫一下outer():

template <typename T> void outer(T& t) {
    inner(t);
}

這個outer()的問題是它不能轉寄右值性質的參數。如果inner()接受參數const int&,inner(5)沒有問題,但outer(5)通不過編譯,T會被推導為int,而int&不能綁定5。

好,讓我們再來嘗試:

template <typename T> void outer(const T& t) {
    inner(t);
}

這樣,如果inner()接受參數為int&,那就違反了常量約束,因此通不過編譯。

如果你可以分別針對T&和const T&重載,outer()確實能夠工作,然後你就能夠像使用inner()一樣使用outer()。

不幸的是,當參數增多時,你需要非常繁瑣的寫一大堆重載函數:T1&和const T1&,T2&和const T2& ..等等。對於每個參數的增加,都會導致重載個數指數級的增加。(VC9 SP1裡面的std::tr1::bind()函數非常牛×的為前5個參數做了這樣的重載,包含了63個重載形式。否則的話,你就得給使用者解釋為什麼不能給函數對象綁定像1729這樣的右值參數。為了產生出這些重載函數,需要使用令人想吐的預先處理機制,噁心到你都不想去碰它)

轉寄的問題在C++98/03中是比較嚴重的,並且本質上無法解決(除了使用令人想吐的預先處理機制,那會顯著的降低編譯速度,並且導致代碼可讀性超差無比)。但是,右值引用優雅的解決了這個轉寄問題。

(我已經在解釋move語義模型之前解釋了初始化和重載判定,但是現在我將先解說一下完美轉寄模型,然後再解釋模板類型推導和引用退化規則。這樣貌似更好一些)

完美轉寄:模型

完美轉寄可以讓你唯寫一個函數模板就能實現接受N個任意類型的參數並且將它們透明的轉寄給任意的函數。它們的常量/變數/左值/右值的屬性都會得到保留,讓你像使用inner()一樣使用outer(),還可以和move語義一起工作而獲得額外的好處(C++0x的可變長模板參數解決了“任意數目的參數”的問題,我們可以把N看做是任意數目)。猛一看可能有點神奇,但其實很簡單:

C:/Temp>type perfect.cpp
#include <iostream>
#include <ostream>
using namespace std;

template <typename T> struct Identity {
    typedef T type;
};

template <typename T> T&& Forward(typename Identity<T>::type&& t) {
    return t;
}

void inner(int&, int&) {
    cout << "inner(int&, int&)" << endl;
}

void inner(int&, const int&) {
    cout << "inner(int&, const int&)" << endl;
}

void inner(const int&, int&) {
    cout << "inner(const int&, int&)" << endl;
}

void inner(const int&, const int&) {
    cout << "inner(const int&, const int&)" << endl;
}

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) {
    inner(Forward<T1>(t1), Forward<T2>(t2));
}

int main() {
    int a = 1;
    const int b = 2;

    cout << "Directly calling inner()." << endl;

    inner(a, a);
    inner(b, b);
    inner(3, 3);

    inner(a, b);
    inner(b, a);

    inner(a, 3);
    inner(3, a);

    inner(b, 3);
    inner(3, b);

    cout << endl << "Calling outer()." << endl;

    outer(a, a);
    outer(b, b);
    outer(3, 3);

    outer(a, b);
    outer(b, a);

    outer(a, 3);
    outer(3, a);

    outer(b, 3);
    outer(3, b);
}

C:/Temp>cl /EHsc /nologo /W4 perfect.cpp
perfect.cpp

C:/Temp>perfect
Directly calling inner().
inner(int&, int&)
inner(const int&, const int&)
inner(const int&, const int&)
inner(int&, const int&)
inner(const int&, int&)
inner(int&, const int&)
inner(const int&, int&)
inner(const int&, const int&)
inner(const int&, const int&)

Calling outer().
inner(int&, int&)
inner(const int&, const int&)
inner(const int&, const int&)
inner(int&, const int&)
inner(const int&, int&)
inner(int&, const int&)
inner(const int&, int&)
inner(const int&, const int&)
inner(const int&, const int&)

太帥了!實現完美轉寄只需要寫兩行!

上面代碼示範了怎樣透明的將t1和t2轉寄給inner();inner()可以知道它們的左值性、右值性、常量性,就像它被直接調用一樣。

就像std::move()一樣,std::identity和std::forward()被定義在C++0x的<utility>標頭檔(VC10中會有);我已經示範了它們是如何?的。(下面我將交替視使用std::identity和我自己實現的Identity,std::forward()和Forward,因為它們實現方法是相同的)

下面,讓我們進行魔法揭秘吧。其實它依賴於模版參數推導和引用摺疊技術。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.