C++11:移动语义和完美转发


拷贝构造和移动构造对比

拷贝构造

代码1:

#include <iostream>
using namespace std;

class HasPtrMem {
public:
    HasPtrMem() : d(new int(0)) {
        cout << "Construct: " << ++n_cstr << endl;
    }

    HasPtrMem(const HasPtrMem &rhs) : d(new int(*rhs.d)) {
        cout << "Copy Construct: " << ++n_cpstr << endl;
    }

    ~HasPtrMem() {
        delete d;
        d = nullptr;
        cout << "Destruct: " << ++n_dstr << endl;
    }

    int *d;
    static int n_cstr;
    static int n_dstr;
    static int n_cpstr;
};

int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cpstr = 0;

HasPtrMem getTemp()
{
    return HasPtrMem();
}

int main()
{
    HasPtrMem a = getTemp();
    return 0;
}

这个程序应该输出

Construct: 1        // getTemp函数中的HasPtrMem()表达式显示调用构造函数,生成临时变量temp
Copy Construct: 1   // 从临时变量temp拷贝构造出一个临时值来作为getTemp函数的返回值
Destruct: 1         // temp析构
Copy Construct: 2   // main中从getTemp函数的返回值拷贝构造出a
Destruct: 2         // getTemp函数的返回值析构
Destruct: 3         // a析构

进行了两次拷贝构造。如果HasPtrMem的指针指向非常大的堆内存数据的话,那么拷贝构造的过程就会非常昂贵。

实际在VS2013环境下输出

Construct: 1
Destruct: 1

这是因为编译器对函数返回值作了优化,即RVO(Return Value Optimization)。

移动构造

移动构造:在构造时使得临时对象的堆内存资源直接“移动”到对象a下,同时保证临时对象不释放其所指向的堆内存。这样在构造完成后,临时对象被析构,a就得到了临时对象所拥有的堆内存资源。

这里写图片描述

代码2:

#include <iostream>
using namespace std;

class HasPtrMem {
public:
    HasPtrMem() : d(new int(0)) {
        cout << "Construct: " << ++n_cstr << endl;
    }

    HasPtrMem(const HasPtrMem &rhs) : d(new int(*rhs.d)) {
        cout << "Copy Construct: " << ++n_cpstr << endl;
    }

    // 移动构造函数
    HasPtrMem(HasPtrMem &&rhs) : d(rhs.d) {
        rhs.d = nullptr;
        cout << "Move Construct: " << ++n_mvstr << endl;
    }

    ~HasPtrMem() {
        delete d;
        d = nullptr;
        cout << "Destruct: " << ++n_dstr << endl;
    }

    int *d;
    static int n_cstr;
    static int n_dstr;
    static int n_cpstr;
    static int n_mvstr;
};

int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cpstr = 0;
int HasPtrMem::n_mvstr = 0;

HasPtrMem getTemp()
{
    HasPtrMem h;    // h构造
    cout << "Resource from " << __FUNCTION__ << ": " << hex << h.d << endl;
    return h;       // h移动构造临时对象,并且h析构
}

int main()
{
    HasPtrMem a = getTemp();    // getTemp函数返回的临时对象移动构造a,并且该临时对象析构
    cout << "Resource from " << __FUNCTION__ << ": " << hex << a.d << endl;
    return 0;       // a析构
}

移动构造函数使用了rhs的成员d初始化本对象的成员d,rhs的成员d随后被置为空指针,这就完成了移动构造的全过程。而拷贝构造函数需要重新分配内存,然后将内容依次拷贝到新分配的内存中。rhs.d = nullptr这句表达式是必须的,因为在移动构造完成之后,临时对象会被立即析构,如果不改变rhs.d的话,则临时对象会析构掉已被转为本对象的堆内存,这样一来本对象中的d指针也成了一个野指针。

这个程序应该输出

Construct: 1
Resource from getTemp: xxxxxxxx
Move Construct: 1
Destruct: 1
Move Construct: 2
Destruct: 2
Resource from getTemp: xxxxxxxx     // 同上面的地址
Destruct: 3

堆内存在getTemp函数返回的过程中,成功地逃避了被析构的厄运。

实际在VS2013环境下输出

Construct: 1
Resource from getTemp: 013652B8
Move Construct: 1
Destruct: 1
Resource from getTemp: 013652B8     // 同上面的地址
Destruct: 2

左值、右值和右值引用

左右值判别方法

  1. 方法一:在赋值表达式中,出现在等号左边的就是左值,而在等号右边的就是右值。
  2. 方法二:可以取地址的、有名字的就是左值,而不能取地址的、没有名字的就是右值。

例如:

a = b + c;

a在等号左边,就是一个左值;b+c在等号右边,则是一个右值。&a是合法的,而&(b+c)却是非法的。

C++11中的右值

在C++11中,右值细分为将亡值(xvalue,eXpiring value)和纯右值(prvalue, Pure Rvalue)。

纯右值就是C++98中右值的概念。常见的纯右值有:返回非引用的函数返回的临时变量值;上面的b+c产生的临时变量值;不跟对象关联的字面值,如:2、“a”、true;类型转换函数的返回值;lambda表达式等。
将亡值则是C++11新增的跟右值引用相关的概念,通常是将要被移动的对象,比如:返回右值引用T&&的函数的返回值、std::move的返回值、转换为T&&的类型转换函数的返回值。


右值引用

概念:在C++11中,右值引用就是对一个右值进行引用的类型。为了区别于C++98中的引用类型,我们称C++98中的引用为左值引用,无论左值引用还是右值引用都必须立即进行初始化。右值通常不具有名字,我们也只能通过引用右值表达式来找到它的存在。

T && a = returnRvalue();

这个表达式中,假设returnRvalue返回一个右值。我们声明了一个名为a的右值引用,其值等于returnRvalue函数返回的临时变量的值。returnRvalue函数返回的右值在表达式语句结束后,其生命周期应该就结束了,但通过右值引用该右值又“重获新生”,其生命周期变得和右值引用类型变量a的生命周期一样长。即a还“活着”,该右值临时变量将会一直存活下去。

T b = returnRvalue();

右值引用变量声明相比于上面这条语句就会少一次对象的构造(b的拷贝构造)和一次对象的析构(returnRvalue()返回的临时变量的析构)。

通常,右值引用不能绑定到任何左值上,比如int c; int && d = c;就无法通过编译。

T & e = returnRvalue();
const T & f = returnRvalue();

e的初始化会导致编译错误,而f则不会。这是因为在C++98中常量左值引用是个“万能的引用类型”,可以使用非常量左值、常量左值、右值对其进行初始化。而且在使用右值对其初始化时,常量左值引用像右值引用一样将右值的生命期延长,不过常量左值所引用的右值在它的“余生”中只能是只读的。

const bool & judgement = true;
const bool judgement = true;

这两个能达到相同的目的,只是前者使用了右值并为其“续命”,而后者的右值在表达式结束后就销毁了。

这里写图片描述


std::move : 强制转换为右值

<utility>中的std::move在能将一个左值强制转换为右值引用,继而我们可以通过右值引用使用该值,以用于移动语义。
基本等同于类型转换:static_cast<T&&>(lvalue);
被std::move转化的左值,其生命期并未改变

在C++11中,拷贝构造/赋值和移动构造/赋值函数必须同时提供,或者同时不提供。这样才能保证类同时具有拷贝和移动语义。
只有移动语义表明该类型的变量所拥有的资源只能被移动,而不能被拷贝。那么这样的资源必须是唯一的,因此只有移动语义的类型往往都是“资源型”的类型,如智能指针、文件流等,标准库中的unique_ptr也是仅可移动的模板类。
有了移动语义,可以实现高性能的swap函数:

template <class T>
void swap(T &a, T &b)
{
    T tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}

noexcept的作用:所修饰的函数在抛异常的时候会直接调用terminate程序终止运行。
std::move_if_noexcept这个函数,在类的移动构造函数没有noexcept关键字修饰时返回一个左值引用从而使变量可以使用拷贝语义,而在有noexcept修饰时返回一个右值引用从而使变量可以使用移动语义。

#include <iostream>
using namespace std;

class MayThrow {
public:
    MayThrow() {}
    MayThrow(const MayThrow & rhs)
    {
        cout << "MayThrow copy constructor." << endl;
    }
    MayThrow(MayThrow && rhs)
    {
        cout << "MayThrow move constructor." << endl;
    }
};

class NoThrow {
public:
    NoThrow() {}
    NoThrow(const NoThrow & rhs)
    {
        cout << "NoThrow copy constructor." << endl;
    }
    NoThrow(NoThrow && rhs) noexcept
    {
        cout << "NoThrow move constructor." << endl;
    }
};

int main()
{
    MayThrow m;
    NoThrow n;

    MayThrow mt = move_if_noexcept(m);  // MayThrow copy constructor.
    NoThrow nt = move_if_noexcept(n);   // "NoThrow move constructor.

    return 0;
}

由于VS2013还不支持noexcept,所以暂时无法验证。


完美转发

概念:完美转发(perfect forwarding,forward的本意指转发、转寄信件等物品)是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

C++11中的引用折叠(reference collapsing)规则:
这里写图片描述
注:记忆方法:一旦定义中出现了左值引用,引用折叠总是优先将其折叠为左值引用。

template <typename T>
void iAmForwarding(T && t)
{
    iExecuteCodeActually(static_cast<T &&>(t));
}

调用iAmForwarding函数时根据传入X类型的引用不同,被实例化如下不同形式:

// 传左值引用
template <typename T>
void iAmForwarding(X& && t)     // X& t
{
    iExecuteCodeActually(static_cast<X& &&>(t));    // X& t
}

// 传右值引用
template <typename T>
void iAmForwarding(X&& && t)    // X&& t
{
    iExecuteCodeActually(static_cast<X&& &&>(t));   // X&& t
}

根据引用折叠规则,t的实际类型就是注释中的类型。这样无论t是左值引用还是右值引用都会没有问题了。
但身为右值引用的t在实际使用时(函数iExecuteCodeActually中的实参)却变成了左值。而我们想在函数调用中继续传递右值,那么久需要使用std::move来进行左右值的转换。而std::move通常等价于static_cast,不过在C++11中用于完美转发的函数不再叫作std::move,而是std::forward。move和forward在实现上差别不大,不过标准库如此设计或许是为了让用途跟名字的含义相对应。因此转发函数可以写成如下形式:

template <typename T>
void iAmForwarding(T && t)
{
    iExecuteCodeActually(std::forward(t));
}

下面举个例子:

#include <iostream>
using namespace std;

void executeCode(int & m) { cout << "lvalue ref." << endl; }
void executeCode(int && m) { cout << "rvalue ref." << endl; }
void executeCode(const int & m) { cout << "const lvalue ref." << endl; }
void executeCode(const int && m) { cout << "const rvalue ref." << endl; }

template <typename T>
void perfectForward(T && t) { executeCode(forward<T>(t)); }

int main()
{
    int a;
    int b;
    const int c = 1;
    const int d = 0;

    perfectForward(a);          // lvalue ref
    perfectForward(move(b));    // rvalue ref
    perfectForward(c);          // const lvalue ref
    perfectForward(move(d));    // const rvalue ref

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值