C++对象移动

C++ 11标准中有一个很重要的特性:可以移动对象,该特性主要针对这样一种场景:一个对象在被拷贝之后就不在使用了或者马上就会被析构掉,这种情况下,使用移动操作而非拷贝操作将会大幅度提升性能。移动操作的思想是接管源对象的内容。

右值引用

为了支持移动操作,新标准引入一种新的引用类型:右值引用,即绑定到右值的引用,使用&&获取右值引用。右值引用的特点是只能绑定到一个即将销毁的对象,这样就可以把一个右值引用的资源移动到另一个对象中。

左值引用和右值引用的区别

左值引用是对左值的应用,右值是对右值的引用,左值的特点是持久,右值的特点是短暂。返回左值引用的函数,赋值,下标,解引用,前置递增/递减运算符,都是返回左值的表达式,可以将左值引用绑定到这些表达式的结果上。返回非引用类型的函数,算数,关系,位以及后置递增/递减运算符,都是返回右值的表达式,可以将右值引用绑定到这些表达式的结果上。
变量都是左值,不能将右值引用绑定到一个右值引用类型的变量上,下列赋值是错误的:

int &&r1 = 42;
int &&r2 = r1;  // 错误

move函数

不能将一个右值引用直接绑定到一个左值上,但是可以显示的将一个左值转化为对应的右值引用类型,通过标准库的move函数实现这一点,该函数定义在头文件中

int &&r3 = std::move(r1); // 正确

调用move之后,除了对r1进行赋值或者销毁之外,不能在使用它,调用move函数之后,不能对移后源对象做任何假设。

移动构造函数和移动赋值运算符

为了使类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符,对应对应的拷贝操作。移动构造函数的第一个参数是该类类型的一个右值引用,和拷贝构造函数一样,任何额外的参数都必须有默认实参。移动构造函数必须确保移后源对象处于这样一个状态:销毁它是无害的。一旦资源完成移动,源对象就不再指向被移动的资源,资源的所有权已经转移了。

class Test
{
public:
    Test(Test &&t) noexcept
        : s(t.s)
    {
        t.s = nullptr;
    }
    // 其它成员
    ... ...
private:
    char *s;
};

移动构造函数不会分配任何新内存,它接管给定的对象的内容,接管之后,源对象中的指针被置为nullptr,如上例中的实例t,在进行移动操作之后,其数据成员s被置为nullptr。
移动赋值运算符和移动构造函数类似:

class Test
{
public:
    operator(Test &&rhs) noexcept
    {
        if (this != &rhs) {
            free(s); // 释放已有元素
            s = rhs.s; // 从rhs接管资源
            rhs.s = nullptr;
        }
        return *this;
    }
    // 其它成员
    ... ...
private:
    char *s;
};

合成的移动操作

与处理拷贝构造函数和拷贝赋值运算符一样,编译器也会合成移动构造函数和移动赋值运算符,但是,合成条件和拷贝操作大不相同。
如果我们不声明自己的拷贝构造函数和拷贝赋值运算符,编译器就会为我们合成这样的操作,但是即使我们不声明自己的移动构造函数和移动赋值运算符,编译器在某些类中也不会合成相应的移动操作,因此有些类就不会有移动构造函数或者移动赋值运算符。
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或者移动赋值运算符:

struct X {
    int i;
    std::string s;
};
struct Y {
    X mem;
};

上述的类X中没有定义任何拷贝控制成员,且i支持移动操作,std::string也定义了自己的移动操作,因此类Y就会有编译器合成的移动操作:

X x;
X x1 = std::move(x); // 使用合成的移动构造函数
Y y;
Y y1 = std::move(y); // 使用合成的移动构造函数

构造函数的匹配规则

如果一个类即有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数,赋值操作类似。拷贝构造和函数接收一个const的左值引用,移动构造函数接收一个非const的右值引用:

Test t1, t2
t1 = t2; // t2是左值,使用拷贝赋值
Test getTest(); // getTest返回一个右值
t2 = getTest(); // 使用移动赋值

如果一个类有拷贝构造函数但是没有定义移动构造函数,这种情况下,编译器不会合成移动构造函数,这样这个类就只有拷贝构造函数但是没有移动构造函数,这种情况下,对其显示调用move操作将拷贝对象:

class Foo {
public:
    Foo() = default;
    Foo(const Foo &); // 拷贝构造函数
    // 其他成员,但不定义移动构造函数
    ... ...
};
Foo x;
Foo y(x); // 拷贝构造函数,x是一个左值
Foo z(std::move(x)); // 拷贝构造函数

对z进行初始化时,调用move(x),它返回一个绑定到x的Foo&&, 此时可以将Foo&&转化为一个const Foo&,因此可以调用拷贝构造函数。

右值引用和成员函数

除了移动构造函数和移动赋值运算符,普通成员函数也可以定义和移动/拷贝构造函数相同参数模式的版本:一个版本接受一个指向const的左值引用,第二个版本接受一个指向非const的右值应用。
例如标准库定义了push_back的两个版本:

void push_back(const X &); // 拷贝
void push_back(X &&); 移动

可以将能转化为类型X的任意对象传递给第一个版本,此版本会从参数拷贝数据。对于第二个版本,只能传递非const的右值。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值