Non-Profit, International

Spirit unsterblich.

C++ 移动语义和 std::move

字数统计:1631 blog

C++11 开始添加了移动语义,伴随着 std::move,对于现代 C++ 有着极为深远的影响。

移动的思想是将对象的所有权进行传递,传统上基于指针的传递并不能满足所有需求:指针只能指向对象或者为空,而移动则是保留对象,将对象的内容设为空,这种方式使得 RAII 一定程度上脱离了原有的函数栈的粗糙控制,使对象本身的声明周期不改变的情况下,更灵活的利用内存。

移动构造和移动赋值

移动构造和移动赋值实际上原理非常简单:若对象是一个内置数据类型或者是由基本数据类型构成的一个类,代表着其所有内存都是在栈上申请的,此时移动构造,移动拷贝和普通构造,普通拷贝保持一致;如果对象中有数据存放在堆区,用指针进行管理,那么就将该指针交给新构造的对象,将旧对象的指针设置为空,其他必要的,在堆中申请内存的类型使用值拷贝。

之前的文章 C++ 左值与右值 简单研究了左值与右值,而移动语义则是让一个左值具有右值的性质:在对某个对象进行移动的过程中,该对象将被视为右值,此时使用一个左值来接收这个右值,并在允许的情况下清空原对象,即完成了对源对象的移动。

具体的实现的代码如下:


#include <utility>
#include <iostream>

class A
{
public:
    A(int num) //构造
    {
        this->num = new int(num);
    }
    A(A &x) //拷贝构造
    {
        this->num = new int(*x.num);
    }
    A(A &&x) //移动构造
    {
        this->num = x.num;
        x.num = nullptr;
    }
    void operator=(A &&x) noexcept //移动赋值
    {
        this->num = x.num;
        x.num = nullptr;
    }
    void operator=(A &x) //普通赋值
    {
        this->num = new int(*x.num);
    }
    ~A() //析构
    {
        delete (this->num);
    }

private:
    int *num;
};

int main()
{
    A a{10};
    A ab = std::move(a);
}

a 经过移动后,将被视为已经删除,此后任何对 a 的手动操作(非编译器操作)都在语义上是不合法的。

值得注意的是,移动构造函数的声明为 A(A &&x),其中 && 不是 and,而是右值引用的标记,换句话说,移动构造函数接收一个类型为 A 的右值引用。

std::move

使用移动语义不光需要在类中实现一个移动构造函数,还要使用 std::move 这个模板,在上面的代码中已经展示了 std::move 的用法。

std::move 的实现非常简单,实际上就只有一条语句:


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

std::remove_reference

其中 std::remove_reference 的实现如下:


template <class T>
using remove_reference_t = typename remove_reference<T>::type;

template <class T>
struct remove_reference
{
    typedef T type;
};
template <class T>
struct remove_reference<T &>
{
    typedef T type;
};
template <class T>
struct remove_reference<T &&>
{
    typedef T type;
};

remove_reference_t 是一个模板别名,用于简化模板的书写。

std::remove_reference 唯一的作用就是得到解除引用后的原本的数据的类型,换句话说,传入 int&int&& 时返回 int,传入 int 时也返回 int

回过头来看 std::move 实现中的 return static_cast<typename std::remove_reference<T>::type&&>(t);,你会发现这句话的意思是解除 t 的引用,获得其类型,并将其变为右值引用,然后用 static_cast 将 t 转换为 t 的类型的右值引用。

以上内容可由下面的代码验证:


#include <iostream>
#include <utility>     // std::move, std::forward
#include <type_traits> // std::is_same, std::remove_reference

template <class T1, class T2>
void print_is_same()
{
    std::cout << std::is_same<T1, T2>() << std::endl;
}

int main()
{
    std::cout << std::boolalpha;

    print_is_same<int, int>();
    print_is_same<int, int &>();
    print_is_same<int, int &&>();

    print_is_same<int, std::remove_reference<int>::type>();
    print_is_same<int, std::remove_reference<int &>::type>();
    print_is_same<int, std::remove_reference<int &&>::type>();

    int a = 0;
    int &c = a;
    int &&b = std::move(a);
    print_is_same<int&&, decltype(std::move(a))>();
    print_is_same<int&&, decltype(std::move(b))>();
    print_is_same<int&&, decltype(std::move(c))>();
}

如果一个类提供了移动构造或者移动赋值,并且在构造或者赋值时使用了 std::move,那么 static_cast 就会调用对应的具有移动语义的重载函数;如果一个类只能移动赋值,不能拷贝,那么 static_cast 会直接选择移动赋值。

参考:

若无特殊声明,本文以 CC BY-SA 3.0 许可协议 提供。