右值引用,完美转发,NRVO 和RVO优化(简单易懂详细)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 右值引用,完美转发,NRVO 和RVO优化(简单易懂详细)

RVO和NRVO优化:

简介:

Return Value Optimization(RVO)和 Named Return Value Optimization(NRVO)是两种优化技术,它们都涉及到函数返回值的优化,可以避免不必要的拷贝构造函数的调用,提高程序性能。

Return Value Optimization(RVO)

RVO 是一种优化技术,其主要思想是将函数内部创建的局部对象直接构造到函数调用方的目标对象中,而不是先在函数内部构造一个临时对象,然后再拷贝到目标对象。这样可以避免额外的构造和拷贝操作,提高性能。

示例代码:

#include <iostream>
class MyClass {
public:
MyClass() {
    std::cout << "Constructor called." << std::endl;
}
MyClass(const MyClass& other) {
    std::cout << "Copy constructor called." << std::endl;
}
};
MyClass createObject() {
    return MyClass();  // RVO 可能会将临时对象直接构造到目标对象,而不进行拷贝。
}
int main() {
    MyClass obj = MyClass();
    MyClass obj = createObject();
    return 0;
}

在上述示例中,如果启用了 RVO,那么 createObject 函数中创建的临时对象可能会直接构造到 obj 中,而不会调用拷贝构造函数。

这样不会调用拷贝构造没有隐式转换的拷贝构造,因为没必要,匿名对象本就是将亡值,拷贝完,之后再把匿名对象析构,多麻烦。因此VS中进行了直接构造.

在GCC中,其基本手段是直接将返回的对象构造在调用者栈帧上,这样调用者就可以直接访问这个对象而不必复制。(其实我也不太理解这段代码是看了一篇大佬的文章复制下来的初学者有个大概的概念就行吧)

原文链接:https://blog.csdn.net/imred/article/details/85492788

Named Return Value Optimization(NRVO)

NRVO 是一种 RVO 的特殊情况,它发生在函数中明确命名的返回值对象上。如果函数返回一个局部对象,并且该对象在函数内部有一个明确的名称,NRVO 可能会将这个局部对象直接构造到调用方的目标对象中。

示例代码:

#include <iostream>
class MyClass {
public:
MyClass() {
    std::cout << "Constructor called." << std::endl;
}
MyClass(const MyClass& other) {
    std::cout << "Copy constructor called." << std::endl;
}
};
MyClass createNamedObject() {
    MyClass obj;
    return obj;  // NRVO 可能会将局部对象 obj 直接构造到调用方的目标对象中,而不进行拷贝。
}
int main() {
    MyClass obj = createNamedObject();
    return 0;
}

在上述示例中,如果启用了 NRVO,那么 createNamedObject 函数中局部对象 obj 可能会直接构造到 obj 中,而不会调用拷贝构造函数。

RVO 和 NRVO 的使用取决于编译器和优化级别,不同的编译器可能有不同的策略。这些优化技术在避免不必要的构造和拷贝操作上起到了关键的作用,提高了程序的性能。

并不是所有的情况都会优化:

在成员函数里面返回this指针就不会优化

例如
#include <iostream>
using namespace std;
class MyClass {
public:
    int data;
    MyClass(int val) : data(val) { cout << "有参构造" << endl; }
    MyClass(const MyClass& val) { this->data = val.data; cout << "拷贝构造" << endl; }
    ~MyClass() { cout << "析构函数" << endl; }
    MyClass addAdd()
    {
        (this->data)++;
        return *this;
    }
};
int main() {
    MyClass obj(42);
    obj.addAdd().addAdd().addAdd();
    std::cout << obj.data << std::endl;
    return 0;
}
输出:

左值、右值(概念)

在C++中,所有的值不是左值,就是右值。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束后就不再存在的临时对象。

区分左值右值方法:

1.有名字的对象都是左值,右值没有名字。

2.还有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。

C++11扩展了右值的概念,将右值分为了纯右值和将亡值。

纯右值:

1.非引用返回的临时变量;

2.运算表达式产生的结果;

3.字面常量(C风格字符串除外,它是地址)。

将亡值:

与右值引用相关的表达式,例如:将要被移动的对象、T&&函数返回的值、std::move()的返回值、转换成T&&的类型的转换函数的返回值。

不懂纯右值和将亡值的区别其实没关系,统一看作右值即可,不影响使用。

示例:
class AA {
  int a;
};
AA getTemp()
{
  return AA();
}
int i = 3;       // ii是左值,3是右值。
int j = i + 8;    // jj是左值,ii+8是右值。
AA a = getTemp();   // a是左值 ,getTemp()的返回值是右值(临时变量)。
左值引用、右值引用

C++98中的引用很常见,就是给变量取个别名,在C++11中,因为增加了右值引用(rvalue reference)的概念,所以C++98中的引用都称为了左值引用(lvalue reference)。

右值引用就是给右值取个名字。(一旦给右值取了名字之后右值将和左值一样,右值可以理解为普通变量了)

语法:数据类型&& 变量名=右值;

示例:

#include <iostream>
using  namespace std;
class AA {
public:
  int a = 9;
};
AA getTemp()
{
  return AA();
}
int main()
{
  int&& x = 3;         // 3是右值。
  int b = 8;             // b是左值。
  int&& c = b + 5;   //  b+5是右值。
  AA&& a = getTemp();   // getTemp()的返回值是右值(临时变量)。
  cout << "a=" << x << endl;
  cout << "c=" << c << endl;
  cout << "aa.m_a=" << a.a << endl;
}

getTemp()的返回值本来在表达式语句结束后其生命也就该终结了(因为是临时变量),而通过右值引用重获了新生,其生命周期将与右值引用类型变量aa的生命周期一样,只要aa还活着,该右值临时变量将会一直存活下去。

引入右值引用的主要目的是实现移动语义。

左值引用只能绑定(关联、指向)左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。

但是,常量左值引用却是个奇葩,它可以算是一个万能的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。

int a = 1;        
const int& ra = a;   // a是非常量左值。
const int b = 1;  
const int& rb = b;  // b是常量左值。
const int& rc = 1;   // 1是右值。
总结

左值引用,使用 数据类型 &, 只能绑定左值。

右值引用,使用 数据类型 &&,只能绑定右值。

已命名的右值引用是左值。

常量左值,使用 const 数据类型 &, 既可以绑定左值又可以绑定右值。

移动语义

如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。

深拷贝把对象中的堆区资源复制了一份,如果源对象(被拷贝的对象)是临时对象,拷贝完就没什么用了,这样会造成没有意义的资源申请和释放操作。如果能够直接使用源对象拥有的资源,可以节省资源申请和释放的时间。C++11新增加的移动语义就能够做到这一点。

实现移动语义要增加两个函数:移动构造函数和移动赋值函数。

移动构造函数的语法:

类名(类名&& 源对象){......}

移动赋值函数的语法:

类名& operator=(类名&& 源对象){……}

注意:

1)对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转义为右值,从而方便使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我用拷贝构造函数,用移动构造函数吧。左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值中的资源,可能会发生意想不到的错误。

2)如果没有提供移动构造/赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/赋值函数就去寻找拷贝构造/赋值函数。

3)C++11中的所有容器都实现了移动语义,避免对含有资源的对象发生无谓的拷贝。

4)移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。

示例:

#include <iostream>
using namespace std;
class AA
{
public:
  int* m_data = nullptr;  // 数据成员,指向堆区资源的指针。
  AA() = default;             // 启用默认构造函数。
  void alloc() {                // 给数据成员m_data分配内存。
    m_data = new int;                       // 分配内存。
    memset(m_data, 0, sizeof(int));   // 初始化已分配的内存。
  }
  AA(const AA& a) {     // 拷贝构造函数。
    cout << "调用了拷贝构造函数。\n";            // 显示自己被调用的日志。
    if (m_data == nullptr) alloc();                     // 如果没有分配内存,就分配。
    memcpy(m_data, a.m_data, sizeof(int));     // 把数据从源对象中拷贝过来。
  }
  AA(AA&& a) {     // 移动构造函数。
    cout << "调用了移动构造函数。\n";            // 显示自己被调用的日志。
    if (m_data != nullptr) delete m_data;         // 如果已分配内存,先释放掉。
    m_data = a.m_data;                                   // 把资源从源对象中转移过来。
    a.m_data = nullptr;                                    // 把源对象中的指针置空。
  }
  AA& operator=(const AA& a) { // 赋值函数。
    cout << "调用了赋值函数。\n";                   // 显示自己被调用的日志。
    if (this == &a)   return *this;                      // 避免自我赋值。
    if (m_data == nullptr) alloc();                     // 如果没有分配内存,就分配。
    memcpy(m_data, a.m_data, sizeof(int));    // 把数据从源对象中拷贝过来。
    return *this;
  }
  //自我复制会造成把自己空间给delete掉
  AA& operator=(AA&& a) { // 移动赋值函数。
    cout << "调用了移动赋值函数。\n";            // 显示自己被调用的日志。
    if (this == &a)   return *this;                      // 避免自我赋值。
    if (m_data != nullptr) delete m_data;         // 如果已分配内存,先释放掉。
    m_data = a.m_data;                                   // 把资源从源对象中转移过来。
    a.m_data = nullptr;                                    // 把源对象中的指针置空。
    return *this;
  }
  ~AA() {                 // 析构函数。
    if (m_data != nullptr) {
      delete m_data; m_data = nullptr;
    }
  }
};
int main()
{
  AA a1;                  // 创建对象a1。
  a1.alloc();             // 分配堆区资源。
  *a1.m_data = 3;   // 给堆区内存赋值。
  cout << "a1.m_data=" << *a1.m_data << endl;
  //AA a2 = a1;         // 将调用拷贝构造函数。
  //cout << "a2.m_data=" << *a2.m_data << endl;
  //AA a3;
  //a3 = a1;              // 将调用赋值函数。
  //cout << "a3.m_data=" << *a3.m_data << endl;
  auto f = [] { AA aa; aa.alloc(); *aa.m_data = 8; return aa; };   // 返回AA类对象的lambda函数。
  AA a4 = f();                // lambda函数返回临时对象,是右值,将调用移动构造函数。
  cout << "a4.m_data=" << *a4.m_data << endl;
  AA a6;
  a6 = f();              // lambda函数返回临时对象,是右值,将调用移动赋值函数。
  cout << "a6.m_data=" << *a6.m_data << endl;
}
看到这大家应该对移动构造熟悉了吧,承接上文的RVO优化和NROV优化

看这样一段代码:

#include <iostream>
class MyClass {
public:
    MyClass() {
        std::cout << "Constructor called." << std::endl;
    }
    MyClass(const MyClass& other) {
        std::cout << "Copy constructor called." << std::endl;
    }
    MyClass(MyClass&& my) = delete;  //进用移动构造
};
MyClass createNamedObject() {
    MyClass obj;
    return obj;  // NRVO 可能会将局部对象 obj 直接构造到调用方的目标对象中,而不进行拷贝。
}
int main() {
    MyClass obj = createNamedObject();
    return 0;
}

报错:

可以看出NROV优化和ROV优化其实利用的就是移动构造和移动复制

注意一旦警用了移动构造那么移动复制也将被禁用

稍微修改一下代码:

#include <iostream>
class MyClass {
public:
    MyClass() {
        std::cout << "Constructor called." << std::endl;
    }
    MyClass(const MyClass& other) {
        std::cout << "Copy constructor called." << std::endl;
    }
    MyClass(MyClass&& my) = delete;
};
MyClass createNamedObject() {
    MyClass obj;
    return obj;  // NRVO 可能会将局部对象 obj 直接构造到调用方的目标对象中,而不进行拷贝。
}
int main() {
    MyClass obj;
    obj = createNamedObject();
    return 0;
}

报错:

完美转发

完美转发是为了解决,右值当作实参传入后变成左值,在函数中不能再次以右值传入别的函数

在函数模板中,可以将参数“完美”的转发给其它函数。所谓完美,即不仅能准确的转发参数的值,还能保证被转发参数的左、右值属性不变。

C++11标准引入了右值引用和移动语义,所以,能否实现完美转发,决定了该参数在传递过程使用的是拷贝语义还是移动语义。

为了支持完美转发,C++11提供了以下方案:

1)如果模板中(包括类模板和函数模板)函数的参数书写成为T&& 参数名,那么,函数既可以接受左值引用,又可以接受右值引用。

2)提供了模板函数std::forward<T>(参数) ,用于转发参数,如果 参数是一个右值,转发之后仍是右值引用;如果参数是一个左值,转发之后仍是左值引用。

示例:

#include <iostream>
using namespace std;
void func1(int& ii) {        // 如果参数是左值,调用此函数。
    cout << "参数是左值=" << ii << endl;
}
void func1(int&& ii) {     // 如果参数是右值,调用此函数。
    cout << "参数是右值=" << ii << endl;
}
 //1)如果模板中(包括类模板和函数模板)函数的参数书写成为T&& 参数名,
 //那么,函数既可以接受左值引用,又可以接受右值引用。
 //2)提供了模板函数std::forward<T>(参数) ,用于转发参数,
 //如果参数是一个右值,转发之后仍是右值引用;如果 参数是一个左值,转发之后仍是左值引用。
//template<typename TT>
//void func(TT&& ii)
//{
//    func1((i);
//}
template<typename A>
void func(A&& i)
{
    func1(forward<A>(i));
}
int main()
{
    int ii = 3;
    func(ii);      // 实参是左值。
    func(8);       // 实参是右值。
}
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
1月前
|
Go 计算机视觉 开发者
全网最全!操作符使用细节全解析
全网最全!操作符使用细节全解析
42 0
|
1月前
|
存储 编译器 C语言
【C++11特性篇】模板的新一力将:可变参数模板 [全解析]
【C++11特性篇】模板的新一力将:可变参数模板 [全解析]
|
9月前
|
存储 编译器
【c++11】万能引用和完美转发
【c++11】万能引用和完美转发
|
9月前
|
C语言
结构体对齐原则在自定义协议解析时的妙用之法
结构体对齐原则在自定义协议解析时的妙用之法
48 0
|
7天前
|
存储 编译器 C++
cpp随笔——浅谈右值引用,移动语义与完美转发
右值引用是C++11引入的关键特性,用于优化资源管理。它分为纯右值(临时对象)和将亡值(即将消失的引用)。右值引用`&&`允许直接访问临时对象,避免拷贝开销。移动语义利用右值引用实现资源转让,提高效率,如在对象构造和赋值时。`std::move`帮助左值转换为右值引用,以利用移动语义。完美转发保持参数的左/右值属性不变,`std::forward`确保在转发时正确处理这些属性。代码示例展示了不同情况下的转发行为。
|
1月前
|
存储 算法 程序员
【C++入门到精通】右值引用 | 完美转发 C++11 [ C++入门 ]
【C++入门到精通】右值引用 | 完美转发 C++11 [ C++入门 ]
28 0
|
1月前
|
存储 编译器 程序员
【C/C++ 基础知识 】深入C++:特殊成员函数的底层原理与规则
【C/C++ 基础知识 】深入C++:特殊成员函数的底层原理与规则
76 0
|
1月前
|
C++ 容器
【C++11特性篇】一文带小白轻松理解【万能引用(引用折叠)】&【完美转发】
【C++11特性篇】一文带小白轻松理解【万能引用(引用折叠)】&【完美转发】
|
7月前
|
C++ 容器
C++新特性:右值引用,移动语义,完美转发
C++新特性:右值引用,移动语义,完美转发
43 0
|
7月前
|
存储 编译器 C++
【C++杂货铺】一文总结C++11新特性:右值引用 | 移动语义 | 完美转发
【C++杂货铺】一文总结C++11新特性:右值引用 | 移动语义 | 完美转发
39 0