作者:Jonas Devlieghere

原文地址:https://jonasdevlieghere.com/guaranteed-copy-elision/

新的 C++ 17 标准带来了很多令人兴奋的新特性,其中一个微小的,不易觉察的改进就是“有保证的复制消除(guaranteed copy elision)”。注意关键字是“有保证”,因为“复制消除(copy elision)”很早就已经成为 C++ 标准的一部分了。尽管它不是像结构化绑定这样显著的语言特性改变,但是我还是很高兴它成为 C++ 标准的新内容。

复制消除(copy elision)

开始讨论这个新标准带来的特性改变之前,不妨先回顾一下 C++ 14 标准中关于复制消除的基本定义,如果你已经了解相关的内容,也知道它是如何工作的,请跳过这一部分。

请看一下下面这个小的类,它的构造函数和析构函数会在每次调用的时候打印一些信息,我将在本文的所有例子中使用这个类的代码。

struct Foo {Foo() { std::cout << "Constructed" << std::endl; }Foo(const Foo &) { std::cout << "Copy-constructed" << std::endl; }Foo(Foo &&) { std::cout << "Move-constructed" << std::endl; }~Foo() { std::cout << "Destructed" << std::endl; }
};

理论上讲,即使存在产生副作用的可能,编译器仍然会在 3 种情况下省略对象的复制/移动构造。

返回值优化(Return Value Optimization,RVO)

最常用的复制消除技术就是返回值优化。如果以传值的方式返回一个对象,复制消除“允许”编译器避免相应的(对象)复制操作。

[…] 在一个返回类型是对象的函数中的 return 语句中,当表达式本身就是 non-volatile 自动(auto)对象(不是函数参数或 catch 块的参数)的名字,并且它与返回值类型一致,且有相同的 cv 限定(cv-unqualified)时,可以通过将这个自动对象直接构造到函数的返回值中来省略一次复制(移动)操作。

命名返回值优化(Named Return Value Optimization,NRVO)

有时,尽管返回值优化适用于任何一种方式,但是常规 RVO 和命名的 RVO 之间还是会有一些差别。下面就是一个使用命名返回值的例子。

Foo f() {Foo foo;return foo;
}int main() { Foo foo = f(); }

返回值优化(Return Value Optimization,RVO)

至于常规的 RVO,如下所示,返回的仅仅是临时对象的值。

Foo f() {return Foo();
}

使用 -fno-elide-constructors 编译选项可以告诉编译器不要执行复制消除的动作,这是 clang 演示这个例子,不过 gcc 也接受一样的编译选项。下面的例子展示了执行复制消除与不执行复制消除的差别(对于 RVO 和 NRVO 的效果是一样的):

$ clang++ foo.cpp -std=c++11 -fno-elide-constructors && ./a.out
Constructed
Move-constructed
Destructed
Move-constructed
Destructed
Destructed$ clang++ foo.cpp -std=c++11 && ./a.out
Constructed
Destructed

这个优化节省了两次(移动)构造函数的调用,第一次复制动作是将局部对象 foo 复制到函数 f() 返回值的临时对象中,第二次复制动作是将函数返回的临时对象复制到 main() 函数中的 foo 对象中。

临时对象传值(Passing a Temporary by Value)

第二种常用复制消除技术的情况是传递一个临时对象的值的时候。

[…] 当一个没有绑定到引用的临时类对象将被复制(移动)到一个具有相同类型的类对象时,可以通过将临时对象直接构造到目标对象的方式省略一次复制(移动)操作。

void f(Foo f) { std::cout << "Fn" << std::endl; }int main() {f(Foo());
}
$ clang++ foo.cpp -fno-elide-constructors -std=c++11 && ./a.out
Constructed
Move-constructed
Fn
Destructed
Destructed$ clang++ foo.cpp -std=c++11 && ./a.out
Constructed
Fn
Destructed

传值的方式抛出或捕获异常(Throwing and Catching Exceptions by Value)

从 C++ 11 开始,抛出或捕捉异常这两种情况也开始支持复制消除。

[…] 在 throw 表达式中,如果操作数是一个 non-volatile 自动对象(不是函数参数或 catch 块参数)的名字,并且这个对象的作用域不超过最内层封闭 try 块(如果有的话)的结尾,则将操作数复制(移动)到异常对象上的操作可以通过直接在异常对象上构造这个自动对象的方式省略掉。

[…] 当一个异常处理 handler 中的异常声明是一个对象,并且这个对象与异常对象类型相同(不考虑 cv 限定)的情况下,如果程序的意图除了对异常声明的对象执行构造函数和析构函数外,不做其他改变,则可以通过将异常声明中的对象看作是异常对象的别名(alias)的方式省略一次复制(移动)动作。

void f() {Foo foo;throw foo;
}int main() {try {f();} catch (Foo foo) {std::cout << "Catch" << std::endl;}
}

让我惊讶的是,无论是 gcc 还是 clang,对这种情况都没有履行复制消除动作,到目前为止我还不知道为什么…

有保证的复制消除(Guaranteed Copy Elision)

有保证的复制消除提案强调了对于当前这种状况(译者注:就是非强制的复制消除策略)的几个问题。如果对复制消除没有保证(仅仅是允许),就无法摆脱拷贝构造函数和移动构造函数被调用的事实,因为消除动作没有发生,这是主要问题。这也阻止了一些不可移动(non-movable)的类型被当作函数返回值传递,比如工厂模式。

尽管在上一节我们已经看到,使用复制消除已经不需要调用拷贝或移动构造函数,但是下面的例子代码还是无法编译(译者注:因为 Foo 的拷贝构造函数和移动构造函数被标记为 delete):

struct Foo {Foo() { std::cout << "Constructed" << std::endl; }Foo(const Foo &) = delete;Foo(const Foo &&) = delete;~Foo() { std::cout << "Destructed" << std::endl; }
};Foo f() {return Foo();
}int main() {Foo foo = f();
}

如果是 C++ 17,上面的代码可以通过编译,并且有相同的输出,就好像拷贝构造函数和移动构造函数还存在一样。这一点是如何做到的?下面就来解释一下。

值的分类(Value Categories)

这个提案的全称是“通过简化的值的分类保证复制消除”。为了实现有保证的复制消除,提案建议区分 prvalue(纯右值)表达式和通过它们初始化的临时对象。更具体地说,一个广义上的左值(generalized lvalue) glvalue 被定义为对象的位置,而一个纯右值 prvalue 被定义为对象的初始值设置者。

如果一个纯右值 prvalue 被用作另一个同一类型对象的初始值设置器(initializer),则直接对这个对象进行初始化,相应的结果就是通过函数返回值(临时对象)的初始化变成了直接初始化(译者注:函数调用表达式左边的对象原本要通过函数返回产生的临时对象初始化,现在变成直接对这个对象进行初始化),避免了拷贝或移动,这就意味着不需要访问对象的拷贝构造函数或移动构造函数。

第二个结果就是即使有了“有保证的复制消除”, C++ 17 中的 NVRO 也不会发生任何变化,正如前面所述,这个更改(译者注:指增加“有保证的复制消除”)只涉及纯右值,对 NVRO 来说,那个命名的值是个广义上的左值(glvalue),所以不受影响。提案的作者也承认了这一点,但是选择将其排除在提案之外。

虽然我们相信可靠的 NRVO 是影响性能的一个重要特征,但是 NRVO 的情况比较微妙,很难简单地给于任何“保证”。

附录:翻译单元(Translation Units)

在 reddit 上的这篇帖子的评论中,有人问复制省略是否会被限制在同一个翻译单元中。很快,用户 flitterio 就通过一个例子说明无论翻译单元的边界在哪里,复制省略都会发生。即使调用者所在的翻译单元不存在被调用的函数,返回值也能被(直接)复制到调用方的堆栈内存中。

附录:拷贝列表初始化(Copy-List-Initialization)

Evgeny Panasyuk 指出,实际上从 C++ 11 开始就可以通过函数返回不可移动(non-movable)的值,如果对象提供了非显式(non-explicit)的构造函数,拷贝列表初始化(Copy-List-Initialization)可以保证不会发生复制或移动动作。

Foo f() {  return {};
}int main() {  auto &&foo = f();
}

这个机制是独立工作的,与复制消除没有任何关系。

clang++ foo.cpp -fno-elide-constructors -std=c++11 && ./a.out
Constructed
Destructed

【广告】更多与现代 C++ 有关的内容,请关注这个公众号

有保证的复制消除(Guaranteed Copy Elision)相关推荐

  1. 浅谈C++11标准中的复制省略(copy elision,也叫RVO返回值优化)

    严正声明:本文系作者davidhopper原创,未经许可,不得转载. C++11以及之后的C++14.17标准均提出一项编译优化技术:复制省略(copy elision,也称复制消除),另外还有RVO ...

  2. C++ Guaranteed Copy Elision

    考虑如下代码: struct S {S(){}// not copyable, not movable:S(S const &) = delete;S(S &&) = dele ...

  3. c++的复制省略(copy elision)

    学习 A simple C++11 Thread Pool 时,发现函数返回了std::future,而std::future的拷贝构造和拷贝赋值都是delete的,感觉有点怪,查了一下,看到 编译器 ...

  4. 【C++】复制省略(Copy elision)

    1. 复制构造函数 1.1 定义 1.2 调用 2. 复制省略 2.1 强制省略复制操作(自C++17起) 2.2 非强制省略复制操作(自C++11起) 1. 复制构造函数 1.1 定义 复制构造函数 ...

  5. C++17之省略不必要的拷贝Copy Elision

    从C++发展历史看来,c++ 17引入了一个规则,要求在满足一定的条件下避免对象的复制,这在以前是可选的.C++17中一个叫做强制复制省略的技术就是基于这个观点的. 至少包括以下两项内容: 1. 返回 ...

  6. 一道面试题:你了解哪些编译器优化行为?知道Copy elision 、RVO吗?

    C++11以后,g++ 编译器默认开启复制省略(copy elision)选项,可以在以值语义传递对象时避免触发复制.移动构造函数.copy elision 主要发生在两个场景: 函数返回的是值语义时 ...

  7. C++ 的复制省略(copy elision)特性

    C++复制省略的介绍: https://zh.wikipedia.org/wiki/%E5%A4%8D%E5%88%B6%E7%9C%81%E7%95%A5 一个有趣的例子: class Test { ...

  8. C++中的RVO、NRVO与Copy Elision

    RVO: Return Value Optimization NRVO: Named Return Value Optimization RVO 和 NRVO 自C++98时代就已存在,即当函数按值返 ...

  9. C++编程法则365条一天一条(358)copy elision(返回值优化NVO和具名返回值优化NRVO)

    文章目录 强制编译器实现的优化 非强制实现优化 参考:https://en.cppreference.com/w/cpp/language/copy_elision Elision 是省略.删节或者忽 ...

最新文章

  1. 2019 年,19 种方法让自己成为更好的 Node.js 工程师
  2. GDCM:gdcm::StrictScanner的测试程序
  3. c遗传算法的终止条件一般_KDD比赛之遗传算法(举例理解)
  4. js传参不是数字_js调用函数时传入的参数个数与函数定义时的参数个数不符时的操作...
  5. 听说读论文也有trick?这篇文章告诉你深度学习论文阅读最佳姿势
  6. Google Gears 体验(1):本机数据库
  7. MyBatis逆向工程生成代码(附源码)
  8. Qt对话框关闭时清理资源
  9. Hi3559AV100移植友方4G模块N720V5(一)
  10. HDU-1212-Big Number【大数】
  11. october php,php – 如何调用组件内的组件[OctoberCMS]
  12. Visual Studio系列创建工程占用空间大的解决办法
  13. POI实现Excel模板下载
  14. rn react native PanResponder手势动画 实现窗口拖动 滑动动画 Animated
  15. MySQL:HINT
  16. 一文助您轻松上手 Kyligence Zen,轻松变身数据达人
  17. 好尚不可为,其况恶乎(转)
  18. 计算机基本操作小技巧
  19. JS实现聊天接收到消息语言自动提醒(您有新的消息请注意查收)
  20. sql文字转换全拼_取汉字全拼的SQL函数

热门文章

  1. 快手适合在美妆行业做广告投放吗?快手广告如何计费?
  2. GraphQL(四):GraphQL工程化实践
  3. Java -- 乒乓球 乒乓弹球游戏
  4. 女孩,既要懂得暧昧,又要懂得拒绝 【20cn 依依】
  5. 【存储知识】文件系统与硬盘存储(分区、格式化、挂载、inode、软链接与硬链接)
  6. 为什么安卓手机没有苹果手机流畅?
  7. SAP ABAP 业务开关和 SAP 电商云的 Feature Level
  8. 为什么说“管理是一门技术、更是一门艺术”
  9. YP.2.7 Other Representations(双语)
  10. 奔骝定位摄影作品之LAS100