[C++] 右值引用和移动语义

深拷贝的性能问题

如果一个类的构造函数申请了堆内存以存储数据,则必然需要在析构函数中释放堆内存,同时需要定义复制构造函数和重载赋值运算符,实现堆内数据的深拷贝。否则可能导致堆内存的二次释放。

但有时候这种深拷贝对于我们的程序来说是非必要的,而且如果存储在堆内的数据量非常大,深拷贝会引起很大的额外性能开销。此时,我们希望有一种方法来避免总是执行非必要的深拷贝。

C++11引入了右值引用移动语义,来避免无意义的深拷贝,提高程序性能。

右值引用

左值是表达式结束后仍然存在的持久对象,右值是指表达式结束时就不存在的临时对象。

一个简单区分方法:如果可对表达式用&符取址,则为左值,否则为右值。

右值没有具体名字,只能通过引用来找到它。

使用&&来声明方法的参数为右值引用类型

先看下面代码:

class A
{
public:
    A()
    {
        cout << "# a new A by native\n";
        n = new int[5];
    }
    ~A()
    {
        cout << "delete A\n";
        delete [] n;
    }

    /* 复制构造函数,传入左值时 */
    A(const A & a)
    {
        cout << "# a new A by copy\n";
        n = new int[5];
        memcpy(n, a.n, 5);  // 深拷贝
    }

    /* 移动构造函数,传入右值时 */
    A(A && a)
    {
        cout << "# a new A by move\n";
        n = a.n;           // 浅拷贝
        a.n = nullptr;
    }
private:
    int *n;
};

// 这样子写是为了避免返回值被优化而导致无法出现右值引用
A getA(bool flag)
{
    cout << "##### into getA now\n";
    A a;
    A b;

    cout << "##### return now\n";
    if(flag)
        return a;
    else
        return b;
}

int main()
{
    {
        cout << "----- copy test\n";
        A a1;
        A a2(a1);      // need copy
    }
    cout << "----- copy over\n";
    {
        cout << "----- move test\n";
        A a3 = getA(true);  // no copy, just move
    }
    cout << "----- move over\n";
}

输出为:

----- copy test
# a new A by native
# a new A by copy		# 此处调用了复制构造函数,进行深拷贝
delete A
delete A
----- copy over
----- move test
##### into getA now
# a new A by native
# a new A by native
##### return now
# a new A by move	# 函数的返回是一个临时变量,显然属于右值,因此对应地调用参数是右值引用的构造函数A(A && a)
delete A
delete A		# 由于是引用,getA()函数返回的对象此时才被释放
delete A
----- move over

可见,利用右值引用定义的构造函数并不需要执行深拷贝,并且由于右值在外部并不会被引用到,所以在构造函数中将其资源直接转移出来是安全的。这就是所谓的移动语义(move),右值引用的一个重要目的是用来支持移动语义。对应的构造函数称为移动构造函数。一般来说还需要进一步重载采用右值引用的赋值运算符,称为移动赋值运算符。

移动语义的move方法

C++11提供了std::move()方法来将左值转换为右值,从而可以使得一般的左值也能够利用移动语义来优化性能。

move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。

示例:

int main()
{
	A a1;
    A a2 = move(a1);
}

输出:

# a new A by native
# a new A by move		# 此时a1已不再拥有资源
delete A
delete A

forward 完美转发

当一个模板函数的参数类型定义为&&右值引用(void func(T && n)),可以传入左值也可以传入右值。当一个右值引用传入该函数时,他在实参中有了命名n,那么此时如果继续向下调用其他函数(将n转发给其他函数),根据C++ 标准的定义,这个参数变成了一个左值。

如下所示,将1作为右值传入后,再将其传入 print,走的却是 print 的左值版本。

template<typename T>
void print(T && n)
{
    cout << "R value " << n << endl;
}
template<typename T>
void print(T & n)
{
    cout << "L value " << n << endl;
}
template<typename T>
void func(T && n)
{
    print(n);
}

int main()
{
	cout << "----------- func(1)\n";
    func(1);
}

输出打印:

----------- func(1)
L value 1

如果继续向下转发的函数其左值版本存在深拷贝,则其对此优化的右值引用版本将不会被调用。此时可以引入完美转发std::forward,恢复变量原来的引用属性。如果n传入前原来是左值,就恢复为左值,如果原来是右值就恢复为右值。

template<typename T>
void func(T && n)
{
    print(n);
    print(forward<T>(n));  	// 完美转发-原来是右值,则转回右值
}

int main()
{
    int n = 10;
    cout << "----------- func(1)\n";
    func(1);
    cout << "----------- func(n)\n";
    func(n);	// n 是左值
}
----------- func(1)
L value 1    # print(n);
R value 1    # print(forward<int>(n));
----------- func(n)
L value 10
L value 10   # after forward , L is still L

emplace_back

对于STL容器,C++11后引入了emplace_back接口,可用于替代push_back,提高性能。

考虑下面的代码:

    vector<string> vs;
    vs.push_back("haha");

尽管 push_back 已经实现了右值引用的版本,但其调用过程中,传入字符串"haha"本身需要先构造一个string临时对象,然后vector内部又要调用一次移动构造函数,完了之后将传入的string临时对象析构。虽然比起传入左值引用而调用复制构造函数的方式要快,但构造和析构这个临时对象仍然显得多余。

c++11可以用 emplace_back 代替 push_back ,emplace_back 可以直接在 vector 中直接构建一个对象,而非创建一个临时对象再放进 vector

所以,以后应尽量使用emplace_back :

    vector<string> vs;
    vs.emplace_back("haha");
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值