C++11 模板 一:彻底理解 std::move 和 std::forward
本文尝试串联 C++11 中的几个基础概念来帮助理解 11 中新增的几个特性及原因。
左值和右值
最初的定义来源于 C 语言,即左值是等号左边可以被赋值的表达式,而右值是等号右边的表达式。
然而在 C++ 中要复杂得多,看了很多资料,没有一个很清晰简单的规定,cppreference 上只是罗列了大量的左值和右值情况。
一般来说,变量、函数这些可以取地址的都是左值,可以理解为一个内存单元;一个数字、字符串字面量是右值因为无法给一个 5 、str + "a"、"hello world" 取地址,可以理解为内存单元里的值。
右值引用
左值引用常规引用(&),事实上就是变量的别名。右值引用(&&)是 C++11 新概念。
std::move
怎么让如上代码最后一行编译成功,很简单,可以用 std::move
移动语义使编译器可以使用移动操作来代替昂贵的拷贝操作, 但注意 std::move 本身并没有执行什么“移动”操作,事实上只是执行了一次类型转换 static_cast<T&&>(lvalue) ,但可以带来代码上的便利。
左值引用和右值引用都可以避免拷贝,但是右值引用更加灵活:
类似地我们可以定义一个移动构造函数:
而通常的赋值构造函数参数是一个 const 变量,无法直接交换内部数据,如果改成非 const 则不支持这种临时构造的写法:
还有一种情况是不可复制但可以移动的对象,比如 std::thread 和 std::unique_ptr ,那么只能用 std::thread A = std::move(B) 的形式来转移对象的所有权。
注意上文只是为了方便语法上的描述,对于 POD 类型,事实上 move 只能执行 copy 操作。此外现在的很多类型已经优化了构造函数可能也不能从 std::move 中获得更多收益,所以有没有性能提升还是看 benchmark。
item 29 (下文的 item 均指 Effective Modern C++)列举了几种C++11的移动语义并无优势情况:
没有移动操作:要移动的对象没有提供移动操作,所以移动的写法也会变成复制操作。 移动不会更快:要移动的对象提供的移动操作并不比复制速度更快。 移动不可用:进行移动的上下文要求移动操作不会抛出异常,但是该操作没有被声明为noexcept (item14)另外不要滥用 std::move ,比如没必要在 函数返回值处使用。
通用引用
前面我们介绍了右值引用的基本情况,接下来我们来看下模板。
上文介绍了一个可以接受右值的函数定义
准确来说,参数定义是一个通用引用,它也可以接受一个左值。
一个通用引用的初始值决定了它是代表了右值引用还是左值引用。如果初始值是一个右值,那么通用引用就会是对应的右值引用,如果初始值是一个左值,那么通用引用就会是一个左值引用。那么具体的类型是怎么被推导的?这里我们就可以引入下文引用折叠的概念。
引用折叠
C++ 代码里是不允许直接定义一个引用的引用,但是由于通用引用的存在,在类型推导时可能会出现一种场景:
如果将模板展开,那么就是:
如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。即,对于一个给定类型X:
· X& &、X& &&和X&& & 都折叠成类型 X&
· 类型X&& &&折叠成X&& (c++ primer)因而实际上等价于
这就实现了左值还是左值,右值还是右值,故称为通用引用。然而还有一种情况,单纯使用通用引用仍然是不够的。
完美转发
“转发”仅表示将一个函数的形参传递——就是转发——给另一个函数。对于第二个函数(被传递的那个)目标是收到与第一个函数(执行传递的那个)完全相同的对象。
来看一种情况:
打印的结果是什么?
T&
T&思考下 问题 1,a 是一个左值还是右值?a 是一个变量,当然就是左值!所以展开方式跟引用折叠一节是一样的!
怎么解决这个问题呢?
假定我们有一些函数f,然后想编写一个转发给它的函数(事实上是一个函数模板)。我们需要的核心看起来像是这样:因此改变一个函数 f
问题就解决了!
补充:条款30 描述了一些转发失败的场景 kelthuzadx/EffectiveModernCppChinese
参考文献
Effective Modern C++ item 23 ~30 C++ primer 第16章Value categories以上就是关于《C++11 模板 一:彻底理解 std::move 和 std::forward》的全部内容,本文网址:https://www.7ca.cn/baike/51203.shtml,如对您有帮助可以分享给好友,谢谢。