C++11中std::move示例

std::move作用:如果其类型支持移动的话,会无条件的将其参数(可能是左值)强制转换为右值引用,从而表示其可以移动,它使得编译器随后能够移动(而不是复制)在参数中传递的值。如果其类型不支持移动,则将进行复制。

因此可以将std::move看着是一个用于提示编译器优化的函数,过去的c++98中,由于无法将作为右值的临时变量从左值当中区别出来,所以程序运行时有大量临时变量白白的创建后又立刻销毁。

std::move定义

	template<class Type>
    typename remove_reference<Type>::type&& move(Type&& Arg) noexcept;
参数说明:
返回值

返回Arg的右值引用,而无论其类型是否是引用类型。

示例代码

#include <iostream>
#include <utility>

class Moveable{
public:
    Moveable() : i(new int(3)) {
        std::cout << "Moveable::Moveable() 构造函数 : ptr(i)=" << i << std::endl;
    }
    ~Moveable() { 
        std::cout << "Moveable::~Moveable() 析构函数 ptr(i)=" << i << std::endl;
        if (i) {
            delete i;
            i = nullptr;
        }
    }
    Moveable(const Moveable & m) : i(new int(*m.i)) {
        std::cout << "Moveable::Moveable(const Moveable &) 拷贝构造函数 ptr(i)=" << i << std::endl;
    }
    Moveable(Moveable && m) {
        this->i = m.i;
        m.i = nullptr;
        std::cout << "Moveable::Moveable(Moveable &&) 移动构造函数 ptr(i)=" << i << std::endl;
    }

    void Set(int x) {
        *i = x;
    }

    const int* Get() {
        return i;
    }
private:
    int* i;
};

void stdmove_test1() {
    Moveable a;
    a.Set(5);
    const int* ptr = a.Get();

    Moveable b(a); // 这里会为b.i重新分配内存

    std::cout << "a ptr=" << a.Get() << "\n";
    std::cout << "b ptr=" << b.Get() << "\n";
    H_TEST_ASSERT(a.Get() == ptr);
    H_TEST_ASSERT(a.Get() != nullptr);
    H_TEST_ASSERT(a.Get() != b.Get());
    H_TEST_ASSERT(b.Get() != nullptr);

    /* Output:
    Moveable::Moveable() 构造函数 : ptr(i)=006AC498
    Moveable::Moveable(const Moveable &) 拷贝构造函数 ptr(i)=006AC4D8
    a ptr=006AC498
    b ptr=006AC4D8
    Moveable::~Moveable() 析构函数 ptr(i)=006AC4D8
    Moveable::~Moveable() 析构函数 ptr(i)=006AC498
    */
}


void stdmove_test2() {
    Moveable a;
    a.Set(5);
    const int* ptr = a.Get();

    Moveable b(std::move(a)); // 会调用移动构造函数,因此不会为b.i重新分配内存,而是直接使用a.i指向的内存

    // 调用 std::move(a) 转换,a.i就变为空指针了。这是需要重点关注的地方


    std::cout << "a ptr=" << a.Get() << "\n";
    std::cout << "b ptr=" << b.Get() << "\n";
    H_TEST_ASSERT(a.Get() != ptr);
    H_TEST_ASSERT(a.Get() == nullptr);
    H_TEST_ASSERT(a.Get() != b.Get());
    H_TEST_ASSERT(b.Get() == ptr);
    H_TEST_ASSERT(b.Get() != nullptr);

    /* Output:
    Moveable::Moveable() 构造函数 : ptr(i)=006AC498
    Moveable::Moveable(Moveable &&) 移动构造函数 ptr(i)=006AC498
    a ptr=00000000
    b ptr=006AC498
    Moveable::~Moveable() 析构函数 ptr(i)=006AC498
    Moveable::~Moveable() 析构函数 ptr(i)=00000000
    */
}

Lvalues 和 Rvalues

每个 C++ 表达式不是左值(Lvalue)就是右值(Rvalue)。左值是指在单个表达式的外部仍然需要保留的对象。可以将左值视为具有名称的对象。所有变量(包括不能更改的 (const) 变量)都是左值。 左值是一个不在使用它的表达式的外部保留的临时值。 若要更好地了解左值和右值之间的区别,请考虑下面的示例:

// lvalues_and_rvalues1.cpp
// compile with: /EHsc
#include <iostream>
using namespace std;
int main()
{
   int x = 3 + 4;
   cout << x << endl;
}

在此示例中,x 是左值,因为它在定义它的表达式的外部保留。 表达式 3 + 4 是为一个右值,因为其计算结果为不在定义它的表达式的外部保留的临时值。 以下示例演示左值和右值的多种正确的和错误的用法:

// lvalues_and_rvalues2.cpp
int main()
{
   int i, j, *p;

   // Correct usage: the variable i is an lvalue.
   i = 7;

   // Incorrect usage: The left operand must be an lvalue (C2106).
   7 = i; // C2106
   j * 4 = 7; // C2106

   // Correct usage: the dereferenced pointer is an lvalue.
   *p = i; 

   const int ci = 7;
   // Incorrect usage: the variable is a non-modifiable lvalue (C3892).
   ci = 9; // C3892

   // Correct usage: the conditional operator returns an lvalue.
   ((i < 3) ? i : j) = 7;
}

Lvalue 引用声明符:&

左值引用其实就是C++11之前我们常说的概念**引用**,表达式为:

Type & cast-expression

Rvalue引用声明符:&&

Type && cast-expression

利用右值引用,您可以将左值与右值区分开。 左值引用和右值引用在语法和语义上相似,但它们遵循的规则稍有不同。

右值引用

右值引用支持移动语义的实现,这可以显著提高应用程序的性能。 利用移动语义,您可以编写将资源(如动态分配的内存)从一个对象转移到另一个对象的代码。 移动语义很有效,因为它使资源能够从无法在程序中的其他位置引用的临时对象转移。

若要实现移动语义,您通常可以向您的类提供移动构造函数,也可以提供移动赋值运算符 (operator=)。其源是右值的复制和赋值操作随后会自动利用移动语义。与默认复制构造函数不同,编译器不提供默认移动构造函数

如果把经由T&&这一语法形式所产生的引用类型都叫做右值引用,那么这种广义的右值引用又可分为以下三种类型:

  1. 无名右值引用
  2. 具名右值引用
  3. 转发型引用

无名右值引用和具名右值引用的引入主要是为了解决移动语义问题。 转发型引用的引入主要是为了解决完美转发问题。

无名右值引用

无名右值引用(unnamed rvalue reference)是指由右值引用相关操作所产生的引用类型。 无名右值引用主要通过返回右值引用的类型转换操作产生,其语法形式如下:

static_cast<T&&>(t)

C++11标准规定该语法形式将把表达式 t 转换为T类型的无名右值引用。 无名右值引用是右值,C++11标准规定无名右值引用和传统的右值一样具有潜在的可移动性,即它所占有的资源可以被移动(窃取)。

具名右值引用

如果某个变量或参数被声明为T&&类型,并且T无需推导即可确定,那么这个变量或参数就是一个具名右值引用(named rvalue reference)。

具名右值引用是左值,因为具名右值引用有名字,和传统的左值引用一样可以用操作符&取地址。 与广义的右值引用相对应,狭义的右值引用仅限指具名右值引用。

传统的左值引用可以绑定左值,在某些情况下也可绑定右值。与此不同的是,右值引用只能绑定右值。

右值引用和左值引用统称为引用(reference),它们具有引用的共性,比如都必须在初始化时绑定值,都是左值等等。

struct X {};
X a;

X&& b = static_cast<X&&>(a);
X&& c = std::move(a);
//static_cast<X&&>(a) 和 std::move(a) 是无名右值引用,是右值
//b 和 c 是具名右值引用,是左值

X& d = a;
X& e = b;
const X& f = c;
const X& g = X();
X&& h = X();
//左值引用d和e只能绑定左值(包括传统左值:变量a以及新型左值:右值引用b)
//const左值引用f和g可以绑定左值(右值引用c),也可以绑定右值(临时对象X())
//右值引用b,c和h只能绑定右值(包括新型右值:无名右值引用std::move(a)以及传统右值:临时对象X())

无名右值引用和具名右值引用的引入主要是为了解决移动语义问题。

关于移动语义(move semantics)的更多解释

为了更好地了解移动语义,请考虑将元素插入 vector 对象的示例。 如果超出 vector 对象的容量时,则 vector 对象必须为其元素重新分配内存,然后将所有元素复制到其他内存位置,以便为插入的元素腾出空间。 当插入操作复制某个元素时,它将创建一个新元素,调用复制构造函数以将数据从上一个元素复制到新元素,然后销毁上一个元素。 然而利用移动语义,可以直接移动对象(没有内存分配、复制)而不必执行成本高昂的内存分配和复制操作。

若要在 vector 示例中利用移动语义,则可以编写移动构造函数,将数据从一个对象移动到另一个对象的

与移动语义相对,传统的拷贝语义(copy semantics)是指某个对象拷贝(复制)另一个对象所拥有的外部资源并获得新生资源的所有权。

可靠编程

若要防止资源泄漏,请始终释放移动赋值运算符中的资源(如内存、文件句柄和套接字)。 若要防止不可恢复的资源损坏,请正确处理移动赋值运算符中的自我赋值。

如果为您的类同时提供了移动构造函数和移动赋值运算符,则可以编写移动构造函数来调用移动赋值运算符,从而消除冗余代码。

参考

  1. msdn move
  2. 如何:编写移动构造函数
  3. Lvalues 和 Rvalues
  4. std::move:强制转化为右值
  5. C++11尝鲜:右值引用和转发型引用