作者:神奇先生
链接:https://www.zhihu.com/question/57048704/answer/151446405
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

编程时经常会写的一种函数叫做named constructor,这种函数的返回值是某个类的实例,其实本质上就是一种构造函数,但是因为可能需要在构建时执行一些其他的步骤,所以没有写成constructor的形式。比如:

User create_user(const std::string &username, const std::string &password) {
User user(username, password);
validate_and_save_to_db(user);
return user;
}
void signup(const std::string &username, const std::string &password) {
auto new_user = create_user(username, password);
login(user);
}

这里create_user就是一个named constructor。

一些c++初学者可能会觉得这个代码不够优化,因为按照这个代码字面意思来理解,create_user创建先创建了一个user,然后返回时又把user赋值给new_user,这个赋值会copy user里面的内容,如果user很大的话(很有可能user里面存了很多信息,比如username这种string的类型),这样太慢(copy string可能还需要多做一次malloc)。

这样难免一些人会想用c++11引入的move来优化,因为create_user里面的user return了之后就没用了,我可以把user move到new_user这样不就省掉了copy时很大的开销了么(比如user里面的username就不需要malloc新内存也不需要一个个字符copy了)。

但事实上,在这种简单的情况下编译器比你更聪明,编译器可以直接把user创建在new_user里,所以user只被创建一次,没有任何copy开销,user和new_user经过编译器优化之后其实是同一个variable!这种优化就叫做copy elision。但是很不幸的是,如果用户想自己用move优化的话,编译器就不用做copy elision了,只能乖乖地按照用户说的来,先创建一个user,然后在调用User的move constructor来创建new_user。这样肯定比前一种开销大很多。这就是为什么clang非常“聪明”地给题主的例子给了一个warning。

接下来我们再说说我们怎么能知道编译器会不会对我写的函数做copy elision的优化呢?有没有可能我写的函数逻辑特别复杂,编译器没法优化呢?如果有的话,我如果写return move(a)不就会比copy更快了吗?

这个逻辑是正确的,编译器其实很傻,一旦create_user里面的逻辑太复杂,编译器可能就没办法分析出你能不能用一个变量取代两个(user和new_user),那它就不做copy elision了。这时候用move就合情合理。

那到底什么时候应该move,什么时候应该依靠copy elision呢?通常主流的编译器都会100% copy elision以下两种情况:

1. URVO(Unnamed Return Value Optimization):函数的所有执行路径都返回同一个类型的匿名变量,比如

User create_user(const std::string &username, const std::string &password) {
if (find(username)) return get_user(username);
else if (validate(username) == false) return create_invalid_user();
else User{username, password};
}

这里所有的return都返回一个User类型,且每个返回的都是一个匿名变量。那编译器100%会执行copy elision。

2. NRVO(Named Return Value Optimization):函数的所有路径都返回同一个非匿名变量,比如

User create_user(const std::string &username, const std::string &password) {
User user{username, password};
if (find(username)) {
user = get_user(username);
return user;
} else if (user.is_valid() == false) {
user = create_invalid_user();
return user;
} else {
return user;
}
}

这里因为所有路径都返回同一个变量user。编译器100%会执行copy elision。

其他的情况编译器可能都不会使用copy elision的优化。

首先 URVO 在 C++17 是强制的。不过 NRVO 不是强制,意味着有时不这么优化也是允许的。
其次,若 return 的表达式是符合返回类型的左值,且编译器没有进行复制省略,那么标准(C++11 开始)也要求编译器先试图把表达式当右值,优先匹配移动构造函数(再匹配通常的复制构造函数),若失败的话则再将其当左值,匹配接受非 const 引用的复制构造函数。

所以按照标准,上面的 std::move(a) 是不必要的(除非你希望强制调用移动构造函数),编译器在必要时会做同样的处理。

作者:神奇先生 &暮无井见铃
链接:https://www.zhihu.com/question/57048704/answer/151453824
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

C++ 函数返回局部变量的std::move()的适用场景(转)的更多相关文章

  1. [转]C语言的那些秘密之---函数返回局部变量

    一般的来说,函数是可以返回局部变量的. 局部变量的作用域只在函数内部,在函数返回后,局部变量的内存已经释放了.因此,如果函数返回的是局部变量的值,不涉及地址,程序不会出错.但是如果返回的是局部变量的地 ...

  2. C语言的那些秘密之---函数返回局部变量[转]

    来源:http://blog.csdn.net/haiwil/article/details/6691854/ 一般的来说,函数是可以返回局部变量的. 局部变量的作用域只在函数内部,在函数返回后,局部 ...

  3. C-基础:函数返回局部变量

    一般的来说,函数是可以返回局部变量的. 局部变量的作用域只在函数内部,在函数返回后,局部变量的内存已经释放了.因此,如果函数返回的是局部变量的值,不涉及地址,程序不会出错.但是如果返回的是局部变量的地 ...

  4. C++函数返回局部变量

    函数不能返回指向栈内存的指针 原因:返回值是拷贝值,局部变量的作用域为函数内部,函数执行结束,栈上的局部变量会销毁,内存释放. 可返回的局部变量: 1. 返回局部变量本身 int sum(int a, ...

  5. CPP笔记_函数返回局部变量

    本篇笔记记录的是关于返回函数中的局部值. 我们知道,在函数中创建的局部变量会随着函数的调用过程的结束,也即其对应函数栈帧的清除,而结束其生命周期.那么,如果我们把这个局部变量返回,就有可能存在该变量对 ...

  6. python函数返回局部变量,局部&全局变量同名问题

    其实关于返回局部变量不只是python的问题,凡是使用堆栈结构处理函数的语言都会有这样的问题,切记不要返回局部变量.因为当创建函数的堆栈撤销,所有对局部变量的修改都灰飞烟灭.来看我的小例子 def h ...

  7. Item 25: 对右值引用使用std::move,对universal引用则使用std::forward

    本文翻译自<effective modern C++>,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 右值引用只能绑定那些有资格被move的对象上去.如 ...

  8. item 23: 理解std::move和std::forward

    本文翻译自<effective modern C++>,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 根据std::move和std::forward不 ...

  9. c++11 std::move()

    简单点理解,c++11 中的std::move() 函数,实际上就是将一个左值强制转换成一个右值引用数据类型,从而可以调用相应的对右值引用重载的函数. 如果使用std::move() 的返回值做为参数 ...

随机推荐

  1. 通过泛型来简化findViewById

    我们一般写findViewById都要加个强制转换,感觉很麻烦,现在你可以在你的BaseActivity中写入如下方法: @SuppressWarnings(“unchecked”) public f ...

  2. Java中IO的简单举例

    package com.kale.io; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStr ...

  3. Datagridview 在基于文本的单元格中启用换行,自动调整行高列宽

    将 DataGridViewCellStyle的 WrapMode 属性设置为 DataGridViewTriState 枚举值之一.下面的代码示例使用 System.Windows.Forms.Da ...

  4. Mysql 区分大小写进行查询

    区分大小写的查询: 因为MySQL的查询是默认不区分大小写的: 如果有些时候需要区分大小写,我们就需要binary这个关键字了. 可以这样用,在stud表中查找sname中带’j’ /’J’: 先不写 ...

  5. Java(C#)基础差异-语法

    1.long类型 Java long类型,若赋值大于int型的最大值,或小于int型的最小值,则需要在数字后加L或者l,表示该数值为长整数,如long num=2147483650L. 举例如下: p ...

  6. WSL(Windows Subsystem for Linux)的安装与使用

    有关WSL的介绍这里就不做解释了.另外,本文仅适用于win10 build 16215以及之后的版本,之前的版本可参考官方链接. (可使用“winver”命令查看windows版本号) 安装: 1.  ...

  7. InnoDB Record, Gap, and Next-Key Locks

    InnoDB has several types of record-level locks including record locks, gap locks, and next-key locks ...

  8. Maven 默认插件以及功能

    Maven 默认插件 已知 Maven 使用 plugin 来执行实际操作的,在默认情况下,Maven 会绑定以下几个插件来完成基本操作. plugin function life cycle pha ...

  9. 卡卡游戏引擎之MVC模式下的事件处理

    前言 在前一篇文章 卡卡游戏引擎快速入门中提到了卡卡游戏引擎采用mvc的开发模式,这里相信介绍一下引擎在mvc模式下是如何做到低耦合的事件处理的. 在卡卡编辑器中选择一个节点,然后在左侧工具栏中切换到 ...

  10. 【使用JSOUP实现网络爬虫】修改数据-设置属性的值

    问题 在你解析一个Document之后可能想修改其中的某些属性值,然后再保存到磁盘或都输出到前台页面. 方法 可以使用属性设置方法 Element.attr(String key, String va ...