深入探索C++对象模型一书中拷贝构造函数和NRV关系探讨

探讨NRV优化机制及拷贝构造函数的作用,分析不同观点并澄清误解。

转自:http://blog.guorongfei.com/2016/01/11/cpp-copy-constructor-nrv/

最近深入探索C++对象模型一书,对于P67中最后一段话的第一句非常不解

这个程序的第一个版本不能实施 NRV 优化,因为 test class 缺少一个 copy constructor

从这段文字来看如果没有拷贝构造函数就不会有 NRV 优化,这一点让人颇为不解,因为从 P66 页中给出的例子来看,NRV 通过额外的引用型参数优化掉了参数的返回,根本没有拷贝构造函数的调用,这个代码和作者自己的论述看上去是自相矛盾的。

对于这个问题,我在三更_雨的博文第二章构造函数语义学--关于NRV优化和copy constructor,找到一些相关的答案,调整了文章行文顺序,放在此处做一个记录。

作者的观点

这个解释来自作者李普曼:

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

这说明其实作者之所以这么说是因为cfront编译器的缘故,不过目前这种编译器并不多见,所以这个论述本身也就不成立了,读者可以直接忽略这个观点。

译者的观点

作者既然给出了解释,其他的解释原本没有太大的必要,但是这些观点对于书的理解有一定的帮助,所以也纪录在此。

这本书的译者侯捷先生在他的 FAQ 中对于这个问题有和读者之间的讨论。过程如下:

leetron给侯捷写信说道:

问题:在67页,最下面两行:这个程式的第一个版本不能实施NRV最佳化,因为test class 缺少一个copy constructor。但是在66页「在编译器层面做最佳化」那一段中所列的码显示,当编译器把xx以__result取代,变成__result.X::X(); 即default constructor被唤起。唤起default constructor 是可以理解的,可是编译器转换后的码并没有使用到 copy constructor呀,为什麽67页最后两行却说缺少一个 copy constructor,就不能实施这个最佳化了呢?

我对上面这个问题做了些解释,但不知我的猜想是否正确。

我的解释是:如同63页与64页「回返值的初始化」这一段,编译器可能将 63页下面的 X bar()函式定义转换成64页的虚拟码,其中有一行__result.X::X(xx); 这会使用到copy constructor。

转换成64页的码后,65页与66页分述了两种后续可能出现的最佳化动作,其中一种即是66 页的编译器层面做最佳化。如此,虽然66页最佳化后的码看起来并不使用到copy constructor,但是这些码是根据像64页那种样子的码(注一)最佳化而来的,而若没有 copy constructor,根本无法转换成64页那种虚拟码,因为其中有一个呼叫copy constructor的动作。所以,虽然 66页经过编译器最佳化的结果省去了 __result.X::X(xx); 这个copy constructor的呼唤动作(因为根本没有xx了),但若没有明白提供一个copy constructor,却无法让编译器进行这样的最佳化。

另一方面,我叁考第5章,205页最下面一段话:「一般而言如果你的设计之中,有许多函式都需要以传值(by value) 传回一个local class object....那麽提供一个copy constructor 就比较合理--甚至即使default memberwise语意已经足够。它的出现会触发 NRV最佳化。然而,就像我在前一个例子中所展现的那样,NRV最佳化后将不再需要唤起 copy constructor,因为运算结果已经被直接计算於「将被传回的object」体内了。」所以,我提出如上所述那个解释,但不确定是否正确,所以e-mail给您以确认一下。

注一:当然,编译器到底怎麽实作这些转换动作,理论上我们是未知的,不能一概而论。所以我写「像64页那种样子的码」。

侯捷给出的答复是:

首先,我要说 leetron 把他的意思描述得非常清楚。在我收到的读者来函中,算是上品— 尤其是描述这麽复杂的思路。

其次,我同意 leetron 说:

转换成64页的码后,65页与66页分述了两种后续可能出现的最佳化动作,其中一种即是 66页的编译器层面做最佳化。

但是我不同意 leetron 这样的看法:

如此,虽然66页最佳化后的码看起来并不使用到copy constructor,但是这些码是根据像64页那种样子的码(注一)最佳化而来的,

我认为,NRV 最佳化并非是由 p63 的原始码而至 p64 的虚拟码,再至 p66 的最佳化。我认为是从 p63 的原始码直接至 p66 的最佳化。所以,似乎可以不需要 copy ctor。

但这麽一来我也无法解释为什麽 lippman 在 p67 最下强调「必须要有 copy ctor 才能实施 NRV 最佳化」。

最后侯捷引用了另外一个读者的论述

黄俊达先生认为:Lippman 在 p67 最后一行所言『这个程式的第一个版本不能实施 NRV 最佳化,因为 test class 缺少一个 copy constructor』,此语错误。黄先生认为如果程式没有 explicit copy constructor,编译器会自动为我们做出来(如为 trivial,则直接 bitwise copy;如为 nontrivial,则由编译器为我们合成出一个 copy constructor )。因此,有没有 explicit copy constructor 并不影响 NRV 最佳化的实施。他认为 NRV 最佳化主要是由编译器 option 来决定要不要实施。他并且做了一些实验,判断 VC 和 gcc 都没有做到 NRV 最佳化,而其不做的理由不是因为技术上的困难,是为了避免造成「user defined copy constructor 之副作用失效」-- 所谓副作用是指,例如「在 user defined copy constructor 中做一个 cout 输出」之类这种「与 memberwise copy 无关」的动作。


移动构造函数是C++11引入的个重要特性,主要用于提高性能。当个对象需要从临时变量或右值赋值给另对象时,传统的拷贝操作会带来不必要的开销,尤其是在处理大型数据结构的情况下。为了解决这个问题,C++新增了“转移语义”,其中就包括**移动构造函数**。 假设我们有个字符串类 `MyString`, 它包含了个指向动态分配的字符数组的指针: ```cpp class MyString { public: char* data; // 构造函数 explicit MyString(const char* str = "") :data(new char[strlen(str)+1]) { strcpy(data, str); } // 拷贝构造函数 MyString(const MyString& other) : data(new char[strlen(other.data)+1]){ strcpy(data,other.data); }; ~MyString() { delete[] data; } }; ``` 如果我们试图返回这样个大的字符串实例作为函数的结果,按照以往的方式将会触发深拷贝过程——即再次申请内存并将原内容复制过去。这种做法不仅低效而且容易引发异常(如果中途失败可能导致资源泄漏)。因此我们需要提供种更高效的办法来代替传统意义上的浅拷贝/深拷贝机制 - 这就是所谓的"移动": ```cpp // 移动构造函数 MyString(MyString&& other) noexcept : data(nullptr){ swap(*this, other); } friend void swap(MyString &first, MyString &second){ using std::swap; swap(first.data, second.data); } ``` 上述代码片段展示了如何编写个简单的移动构造函数。它接受个 rvalue 引用参数 (`MyString &&`) ,并且避免了切形式的数据副本创建动作,仅仅交换两个对象之间的所有权即可完成迁移任务。此外还定义了个友元函数 `swap()` 方便实施具体的元素互换步骤。最后别忘了加上关键字 **noexcept**, 表明此特殊成员函数绝不可能抛出任何形式的 runtime exception. ### 示例应用 现在考虑这么段演示代码: ```cpp MyString CreateBigString(){ const int size=1e6; MyString ret("A"); for(int i=0;i<size-1;++i) strcat(ret.data," "); return ret;//RVO/NRV优化可能会生效直接构建目标位置而不是先生成局部再搬移出去. } int main(){ auto bigstr=CreateBigString();//如果没有move semantics则此处会发生昂贵的deep copy!!! } ``` 可以看到在现代编译器支持下很多场景都可以享受 RVO(Return Value Optimization) 或 NRV(Named Return Value optimization),即使如此明确写出 move 也十分必要以确保兼容性跨平台稳定性。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值