现在的位置: 首页 > 综合 > 正文

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

2012年06月06日 ⁄ 综合 ⁄ 共 11306字 ⁄ 字号 评论关闭

 

零度の冰翻译,原文地址在此,转载请注明出处

【翻译】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()。已经有了一些不错的解决方案,比如工厂方法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,因为它们实现方法是相同的)

下面,让我们进行魔法揭秘吧。其实它依赖于模版参数推导和引用折叠技术。

抱歉!评论已关闭.