首页 > 其他分享 >具名返回值优化(NRV)

具名返回值优化(NRV)

时间:2022-10-24 18:56:32浏览次数:74  
标签:int 具名 mov Test 返回值 拷贝 NRV 构造函数

具名返回值优化(NRV)

最近在看《深度探索C++对象模型》,在第2.3节中提到了具名返回值优化(Named Return Value optimization, NRV),如下:

#include <iostream>

class Test{
public:
  explicit Test(int i)
  	: m_i(i)
    {
        std::cout << "Test::Test(int)" << std::endl;
    }

    Test(const Test& rhs)
        : m_i(rhs.m_i)
    {
        std::cout << "Test::opeartor=(const Test&)" << std::endl;
    }

    ~Test()
    {
        std::cout << "Test::~Test()" <<  std::endl;
    }
private:
  int m_i;
};

Test getTest()
{
  Test t(1);
  return t;
}

int main()
{
  auto test = getTest(); 
}

在代码33行中,main()函数使用getTest()函数的返回值进行初始化,而getTest()函数的返回值为类Test,按理来说应该会返回一个临时对象tmp,然后在33行中调用test的拷贝构造函数,然后tmp对象析构掉。而在getTest()函数中,也有一个从t到临时对象tmp的拷贝构造和t的析构。即:

  • getTest()t构造函数
  • 临时对象tmp拷贝构造
  • getTest()t析构函数
  • main()函数中test对象拷贝构造
  • 临时对象tmp析构
  • main()函数中test对象析构。

即期望的输出应该是类似于:

Test::Test(int)
Test::opeartor=(const Test&)
Test::~Test()
Test::opeartor=(const Test&)
Test::~Test()
Test::~Test()

但在实际情况中,g++(g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0)的输出确是:

Test::Test(int)
Test::~Test()

事实上,如果一个函数中所有return语句的返回值都是同一个具名对象(如getTest()return语句返回的都是对象t),那么编译器会省略掉其拷贝构造函数,即变成如下代码:

void getTest(Test* pTest)
{
    ::new (pTest) Test(1); 
    // placement new, 即在pTest处调用构造函数
}

int main()
{
    char tmp[sizeof(Test)]; // 只分配内存,不调用构造函数
    getTest(reinterpret_cast<Test*>(tmp));
    Test& t = *(reinterpret_cast<Test*>(tmp));
}

在这种情况下,只会调用一次构造函数,一次析构函数,而没有拷贝构造函数的调用。
而g++支持禁用掉返回值优化,只需要添加编译参数-fno-elide-constructors即可,得到的结果如前所示,调用了多次拷贝构造和析构函数。

书中一处描述现在可能已不适用

在《深度探索C++对象模型》中提到类需要显式定义的拷贝构造函数时才会触发NRV,但在测试中g++即使类不显式定义拷贝构造函数也会触发NRV。

class TestWithoutCopyCtor {
public:
  TestWithoutCopyCtor(int i)
  	: m_i(i)
    { }
private:
  int m_i;
};

class TestWithCopyCtor {
public:
  TestWithCopyCtor(int i)
    :m_i(i)
    {}
  TestWithCopyCtor(const TestWithCopyCtor& rhs)
    :m_i(rhs.m_i)
    {}
 private:
  int m_i;
};

TestWithCopyCtor getTestWith()
{
  TestWithCopyCtor t(1);
  return t;
}

TestWithoutCopyCtor getTestWithout()
{
  TestWithoutCopyCtor t(2);
  return t;
}

int main()
{
  auto t1 = getTestWith();
  auto t2 = getTestWithout();
}

gcc.godbolt.org上使用其中x86-64 gcc 12.2,不添加任何编译参数,getTestWith()和getTestWithout()函数汇编如下:

getTestWith():
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     esi, 1
        mov     rdi, rax
        call    TestWithCopyCtor::TestWithCopyCtor(int) [complete object constructor]
        nop
        mov     rax, QWORD PTR [rbp-8]
        leave
        ret
getTestWithout():
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        lea     rax, [rbp-4]
        mov     esi, 2
        mov     rdi, rax
        call    TestWithoutCopyCtor::TestWithoutCopyCtor(int) [complete object constructor]
        mov     eax, DWORD PTR [rbp-4]
        leave
        ret

可以看到无论有没有显式定义的拷贝构造函数,都会触发NRV。

网上找到了这样一段话:

如果客户没有显示提供拷贝构造函数,那么cfront认为客户对默认的逐位拷贝语义很满意,由于逐位拷贝本身就是很高效的,没必要再对其实施NRV优化;但 如果客户显式提供了拷贝构造函数,这说明客户由于某些原因(例如需要深拷贝等)摆脱了高效的逐位拷贝语义,其拷贝动作开销将增大,所以将应对其实施NRV 优化,其结果就是去掉并不必要的拷贝函数调用。

返回值优化(RVO)

在wiki上可找到Copy elision(拷贝省略)词条,其中介绍了返回值优化(Return Value Optimization, RVO),上面的NRV就是RVO的一个例子。其余情况如(示例来自wiki):

  • 函数直接返回一个匿名对象
#include <iostream>

struct C {
  C() = default;
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C();
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f();
}
  • 还有一个拷贝省略的例子:生成一个匿名临时对象,用来初始化一个对象(c2)
#include <iostream>

int n = 0;

struct C {
  explicit C(int) {}
  C(const C&) { ++n; }  // the copy constructor has a visible side effect
};                      // it modifies an object with static storage duration

int main() {
  C c1(42);      // direct-initialization, calls C::C(int)
  C c2 = C(42);  // copy-initialization, calls C::C(const C&)

  std::cout << n << std::endl;  // prints 0 if the copy was elided, 1 otherwise
}

标签:int,具名,mov,Test,返回值,拷贝,NRV,构造函数
From: https://www.cnblogs.com/lycpp/p/16822425.html

相关文章

  • resultType="int" 返回值是 null
    在xml中写SQL语句时,指定的resultType="int"当mysql中没查到数据时返回值不是-1,也不是0,而是null.原因:mybatis对resultType的官方描述期望从这条语......
  • 返回值类型后置
    转自:https://blog.csdn.net/m0_51955470/article/details/117960694 #include<iostream>usingnamespacestd;//template<typenameR,typenameT,typenameU>//R......
  • 函数调用时的return返回值与throw抛出值
    return是如何将值返回给主调函数的我们知道,被调函数运行结束后才会返回主调函数,但是被调函数运行结束后系统为被调函数中的局部变量分配的内存空间就会被释放。也就是说,re......
  • 关于ToString()、ToKnowColor()及属性返回值为类类型的理解
    1usingSystem;2usingSystem.Collections.Generic;3usingSystem.Linq;4usingSystem.Text;5usingSystem.Drawing;6usingSystem.Windows.Forms......
  • JS map 三种不同的写法返回值
    functioncreateData(){//如果不fill循环默认会跳过空值returnnewArray(1000).fill(null).map((v,i)=>({name:`name${i+1}`}));}functionC......
  • 多返回值、多种参数使用形式
    多个返回值按照返回值的顺序,写对应顺序的多个变量接收即可变量之间用逗号隔开支持不同类型的数据return语法位置参数:调用函数时根据函数定义的参数位置来传递......
  • mybatis返回值乱码(mysql数据库编码正确)
    解决办法:在springmvc.xml文件里面插入<!--解决乱码问题--><mvc:annotation-driven><mvc:message-converters><beanclass="org.springfram......
  • C# 7.0 新特性: 基于Tuple的“多”返回值方法
    C#7.0新特性1:基于Tuple的“多”返回值方法回顾首先,提出一个问题,C#中,如何使一个方法可返回"多个"返回值?我们先来回顾一下C#6.0及更早版本的做法。 在C#中,通常我们......
  • python 函数的多个返回值,多种传参方式
    1.函数的多个返回值deftest_return():return1,2按照返回时顺序,写对应顺序的多个变量接手即可变量之间用逗号隔开支持不同类型的数据return2.函数的多种传参......
  • C++函数的返回值——返回引用类型&非引用类型的区别
    本文参考了C++函数的返回值——返回引用类型&非引用类型要搞清楚这个问题我们必须要先搞清楚return的时候发生了什么?我们有一个类如下(不需要仔细看)12345678910111......