C++精要分析右值引用与完美转发的应用

目录
  • 区分左值与右值
  • 右值引用
  • 移动语义
  • 完美转发
  • 结语

区分左值与右值

在C++面试的时候,有一个看起来似乎挺简单的问题,却总可以挖出坑来,就是问:“如何区分左值与右值?”

如果面试者自信地回答:“简单来说,等号左边的就是左值,等号右边的就是右值。” 那么好了,手写一道面试题继续提问。

int a=1;
int b=a;

问:a和b各是左值还是右值?

b是左值没有疑问,但如果说a在上面是左值,在下面是右值的,那就要面壁思过了。C++从来就不是一门可以浅尝辄止的编程语言,要学好它真的需要不断地去探问。公布答案:上面代码中的a和b都是左值。所以在很多地方都能看到的区分左右值说法是并不准确的。

如果是给出描述性的说明,那么左值就是指向特定内存具有名称的值(具名对象),它有一个相对稳定的内存地址,并且有一段较长的生命周期。右值是不指向稳定内存地址的匿名值(不具名对象),它的生命周期很短,通常是暂时性的。

要是看着上面这段说明有些抽象,那还有一个好办法来帮助区分,那就是是否可以用取地址符“&”来获得地址。如果能取到地址的则为左值,否则编译期都报错的,那就是右值。

还是以上面的代码为例,&a; &b;这个一眼能看出来可以取地址成功,这是左值。而&1这样的写法编译器肯定会报错,所以1是右值。用这样的方法,目测也可以判断出来了。

右值引用

说到C++中的引用,相信大家都很熟悉其用法了。在函数调用时需要对变量进行修改,或者避免内存复制,就会使用引用的方式。当然,使用指针也能达到一样的效果,但引用相对来说更为安全可靠。这种使用方式就是左值引用。

那么好了,我们先从语法上来认识一下右值引用。

int i = 0;
int &j = i; //左值引用
int &&k = 10; //右值引用

我们看到,右值引用的写法就是在变量名前加上"&&"标识。它的作用是可以延长字面量数字10的生命周期。不过,这看起来似乎并没什么用,不像左值引用那样已经深入人心。那么,我们接下来看一段有意义的示例代码。

#include        <iostream>
using namespace std;
static const int DataSize = 1024;
class ActOne {
    public:
        ActOne() { cout << "ActOne default construct" << endl; }
        ActOne(const ActOne &one) { cout << "ActOne copy construct" << endl; }
        ~ActOne() { cout << "ActOne destructor" << endl;}
        void DoSomething() { cout << "ActOne work" << endl; }
};
ActOne make_one() {
    ActOne one;
    return one;
}
int main() {
    ActOne one = make_one();
    one.DoSomething();
    cout << "++++++++++" << endl;
    ActOne &&one2 = make_one();
    one2.DoSomething();
}

上述源码就是实现生成一个对象并返回的功能。需要注意的是,如果使用g++编译器,对这段代码进行编译的时候要加上-fno-elide-constructors以屏蔽编译器对构造函数的优化操作。

再来看下运行结果:

ActOne default construct
ActOne copy construct
ActOne destructor
ActOne copy construct
ActOne destructor
ActOne work
++++++++++
ActOne default construct
ActOne copy construct
ActOne destructor
ActOne work
ActOne destructor
ActOne destructor

经过对比,我们可以发现未使用右值引用的写法中,拷贝构造函数执行了两次,因为这是make_one()中的return one;会复制一次构造产生的临时对象,接着在ActOne one = make_one();语句中将临时对象复制到one变量,这是第二次拷贝构造的调用。

那么,使用了右值引用的方法中,拷贝构造函数只调用了一次,one2实际上指向的是一个临时存储的变量。因为这个临时变量被one2作为右值所引用,因此其生命期也延长到main函数结束才调用解析构造方法。

大家可以好好体会一下右值引用的作用,对于性能敏感的C++程序员来说,它不仅是降低了程序运行的开销,而且临时局部变量的可引用,也意味着可以减少动态分配内存所带来的管理复杂度。

移动语义

可能有同学出于对技术的追求,会继续提问:那我还想优化程序性能,再减少一次拷贝构造函数的开销行不行?应当对这样的提问给予积极的回应,答案是可以的,这就是C++11标准所引入的移动语义。

让我们将上一节的代码稍加改动,然后来体会一下移动语义的使用。main函数和make_one函数没有变化,所以仅列出ActOne类的源码。

class ActOne {
    public:
        ActOne():data_ptr(new uint8_t[DataSize]) { cout << "ActOne default construct" << endl; }
        ActOne(const ActOne &one) { cout << "ActOne copy construct" << endl; }
        ActOne(ActOne &&one) { // 移动构造方法
            cout << "ActOne move construct" << endl;
            data_ptr = one.data_ptr;
            one.data_ptr = nullptr;
        }
        ~ActOne() {
            cout << "ActOne destructor" << endl;
            if (data_ptr != nullptr) {
                delete []data_ptr;
            }
        }
        void DoSomething() { cout << "ActOne work" << endl; }
    private:
        uint8_t *data_ptr;
};

我想对于任何一名写C/C++的代码的程序员来说,最大的愿望就是动态内存的分配和释放次数越少越好。源码中的ActOne(ActOne &&one)就是一个移动构造方法,它接受的是一个右值作为参数,通过转移实参对象的数据以实现构造目标对象。如果是复制构造要怎么做?那就要先为data_ptr分配好内存,然后再调用内存拷贝函数memcpy进行一次DataSize字节数的复制。

相比于复制构造方法,移动构造只需要进行指针值的替换即可,其时空消耗是不可同日而语的。程序添加了一个移动构造方法运行之后的结果如下:

ActOne default construct
ActOne move construct
ActOne destructor
ActOne move construct
ActOne destructor
ActOne work
++++++++++
ActOne default construct
ActOne move construct
ActOne destructor
ActOne work
ActOne destructor
ActOne destructor

从上面的结果可以观察到,在右值引用和移动语义的配合下,内存的分配实际只发生了一次,移动构造也只有一次。大家可以往上翻到上一节的程序打印结果,对比一下纯拷贝式的构造,进行了三次内存的分配,两次内存深复制操作。这对于程序性能的影响已经不用多说了,各位可以进行benchmark测试以验证移动语义带来的提升了。

从构造函数的优先级来说,编译器对于右值会优先使用移动构造函数去生成目标对象,如果移动构造函数不存在,则是使用复制构造函数。那么赋值运算符能不能进行移动操作呢?答案是可以的,这个实现就留给各位自己去尝试吧。

提示一下,赋值运算符函数的声明:

ActOne & operator=(ActOne &&one) {……}

完美转发

我们再来学习C++11中的一个新特性,就是万能引用。何谓万能,这个名称很唬人,其实就是一种引用的实现方法,它既可以引用左值,也可以引用右值。不废话,还是直接上代码。

int get_param() { return 100;}
int &&a = get_param(); // a为右值引用
auto &&b = get_param(); // b为万能引用

可以看到,a和b的区别就在于b的类型是由auto推导而来,而a则是确定类型的。这是作为函数返回值的,再看一个模板参数的例子:

template <class T>
void func1(T &&t){} // t为万能引用
int a = 100;
const int b = 200;
func1(a);
func1(b);
func1(get_param());

模板方法的参数t可以接受任何类型的数据,并推导出一个引用类型结果,是什么结果我们后面会说。所以我们会发现,万能引用本质上是发生了类型推导。auto &&T &&在初始化过程中都会发生类型推导。

那么推导结果的规则也很简单:

  1. 如果源对象是左值,则目标对象会被推导为左值引用;
  2. 如果源对象是右值,则目标对象会被推导为右值引用。

万能引用的概念大家已经了解,那么它的用途是什么呢?这就是本节标题所要说的完美转发。实话说,我不太喜欢C++术语中的某些翻译,在中文语境下很容易让人费解、误解或是产生不必要的期待。例如C++的万能引用可以实现完美转发,如果你向一名初学者来上这么一句,他是不是会觉得“这门语言也太牛X了吧,竟然有万能和完美的特性?” 窃以为换成“全值引用”和“任意转发”会不会低调和贴切一些呢。

让我们先从转发的一个局限性示例说起:

template<class T>
void show_info(T t) {
    cout << "type is: " << typeid(t).name() << endl;
}
template<class T>
void transform(T t) {
    show_info(t);
}
int main() {
    string tmp("test for forward");
    transform(tmp);
}

上述代码可以工作,但从性能上说string类对象作为参数传递时会发生一次临时对象复制。在实际工作中,它可能就是一个包含有大块内存变量的对象,显然不能这么干。那就给参数加上一个&符使之成为左值引用吧。下一个问题又来了,如果传的参数是个右值怎么?看到这里,大家就明白了,要想结束抬杠在这儿用上万能引用就好了。

最终版完美引用实现,仅列出有变动的代码:

template<class T>
void transform(T &&t) {
    show_info(std::forward<T>(t));
}

std::forward()是标准库中的模板方法,它的功能就是可以根据值的类型将其按左值引用或右值引用进行转发。这样,既避免了临时对象复制的开销,又可以支持任意类型的对象转发。某种意义上,将其称为“完美”似乎也并不为过。毕竟要让挑剔的C++程序员感到满意并不容易啊。

需要注意的是,标准库中的std::move()方法是将任意实参转换为右值引用,使用这个方法不需要指定模板实参。而std::forward()方法在使用的时候必须指定模板实参,也只有它才能按实际类型进行转发。

结语

右值引用说到这里,相信大家已经从一知半解的状态到可以理解并运用了。它对于苛求性能以及强调效率的场景有着非凡的意义,例如在基础库组件的实现中。虽然大多数程序员都不一定会参与到基础库的开发中,但这就看个人对于技术之道的追求了。即使是调用别人做好的库来组装一个应用,也会遇到性能调优的问题,那个时候你对老板有多大的价值就体现在这里了。

如果大家在工作中发现以前的代码在用支持C++11的编译器重新编译之后,运行效率居然有了提升,不用奇怪,这就是基于C++11的新特性做的编译期优化。例如今天学习的右值引用、移动语义、万能引用、完美转发等就在语法层面提供了良好的支持。

希望我们接下来在实践中不断练习,能够发挥出C++的最大威力来!

到此这篇关于C++精要分析右值引用与完美转发的应用的文章就介绍到这了,更多相关C++右值引用与完美转发内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅析C++11中的右值引用、转移语义和完美转发

    1. 左值与右值: C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值. 可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值. 从本质上理解,创建和销毁由编译器幕后控制的,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象),例如: int& fo

  • C++左值与右值,右值引用,移动语义与完美转发详解

    目录 C++——左值与右值.右值引用.移动语义与完美转发 一.左值和右值的定义 二.如何判断一个表达式是左值还是右值(大多数场景) 三.C++右值引用 四.std::move()与移动语义 五. 完美转发 总结 C++——左值与右值.右值引用.移动语义与完美转发 在C++或者C语言中,一个表达式(可以是字面量.变量.对象.函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式. 一.左值和右值的定义 1.左值的英文为locator value,简写为lvalue,可意为存储在内存中.有明

  • C++精要分析右值引用与完美转发的应用

    目录 区分左值与右值 右值引用 移动语义 完美转发 结语 区分左值与右值 在C++面试的时候,有一个看起来似乎挺简单的问题,却总可以挖出坑来,就是问:“如何区分左值与右值?” 如果面试者自信地回答:“简单来说,等号左边的就是左值,等号右边的就是右值.” 那么好了,手写一道面试题继续提问. int a=1; int b=a; 问:a和b各是左值还是右值? b是左值没有疑问,但如果说a在上面是左值,在下面是右值的,那就要面壁思过了.C++从来就不是一门可以浅尝辄止的编程语言,要学好它真的需要不断地去

  • C++11 模板参数的“右值引用”是转发引用吗

    在C++11中,&&不再只有逻辑与的含义,还可能是右值引用: void f(int&& i); 但也不尽然,&&还可能是转发引用: template<typename T> void g(T&& obj); "转发引用"(forwarding reference)旧称"通用引用"(universal reference),它的"通用"之处在于你可以拿一个左值绑定给转发引用

  • 深入了解c++11 移动语义与右值引用

    1.移动语义 C++11新标准中一个最主要的特性就是提供了移动而非拷贝对象的能力.如此做的好处就是,在某些情况下,对象拷贝后就立即被销毁了,此时如果移动而非拷贝对象会大幅提升性能.参考如下程序: //moveobj.cpp #include <iostream> #include <vector> using namespace std; class Obj { public: Obj(){cout <<"create obj" << e

  • C++11语法之右值引用的示例讲解

    目录 一.{}的扩展 initializer_list的讲解: 跳转initializer_list实现 二.C++11一些小的更新 decltype nullptr 范围for 新容器 三.右值引用 右值真正的用法 完美转发 默认成员函数 总结 一.{}的扩展 在原先c++的基础上,C++11扩展了很多初始化的方法. #include<iostream> using namespace std; struct A { int _x; int _y; }; int main() int a[]

  • 详解C++11中的右值引用与移动语义

    C++11的一个最主要的特性就是可以移动而非拷贝对象的能力.很多情况都会发生对象的拷贝,有时对象拷贝后就立即销毁,在这些情况下,移动而非拷贝对象会大幅度提升性能. 右值与右值引用 为了支持移动操作,新标准引入了一种新的引用类型--右值引用,就是必须绑定到右值的引用.我们通过&&而不是&来获得右值引用.右值引用一个重要的特性就是只能绑定到将要销毁的对象. 左值和右值是表达式的属性,一些表达式生成或要求左值,而另一些则生成或要求右值.一般而言,一个左值表达式表示的是一个对象的身份,而右

  • C++标准之(ravalue reference) 右值引用介绍

    1.右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题.但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了CopyElision.RVO(包括NRVO)等编译器优化技术,它们可以防止某些情况下临时对象产生和拷贝.下面简单地介绍一下CopyElision.RVO,对此不感兴趣的可以直接跳过: (1)CopyElision CopyElision技术是为了防止某些不必要的临时对象产生和拷贝,例如: 复制代码 代码如下: structA{ A(

  • 深入解读C++中的右值引用

    右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一,这点从该特性的提案在C++ - State of the Evolution列表上高居榜首也可以看得出来. 从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题.从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷.从库设计者的角度讲,它给库设计者又带来了一把利器.从库使用者的角度讲,不动一兵一卒便可以获得"免费的"效率提升- 在标准C++语言中,临时量(术语为右值,因其出

  • 浅谈C++左值引用和右值引用

    实例如下: #include<iostream> #include<utility> #include<vector> using namespace std; int f(); int main() { vector<int>vi(100); int i=42; int &&r1=i;//error不能把右值引用绑到左值上 int &&r2=10; int &r3=i; int &r4=10;//error非

  • C++11右值引用和std::move语句实例解析(推荐)

    右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一.从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题.从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷.从库设计者的角度讲,它给库设计者又带来了一把利器.从库使用者的角度讲,不动一兵一卒便可以获得"免费的"效率提升- 下面用实例来深入探讨右值引用. 1.什么是左值,什么是右值,简单说左值可以赋值,右值不可以赋值.以下面代码为例,"A a = getA();&q

随机推荐