Effective Modern C++(6): 右值引用、移动、完美转发(1)
source link: https://keys961.github.io/2022/06/12/Effective-Modern-C++(6)/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
1. std::move
与std::forward
2个函数实际上只做了类型转换,运行时什么都不做。
-
std::move
:接受一个通用引用,返回一个右值引用- 若不想对象移动,声明为
const
类型,传参时不会调用移动构造(因为移动构造没有const
),而是拷贝构造
- 若不想对象移动,声明为
-
std::forward
:若变量传入的是左值,则转化为左值;否则转化为右值-
注意,是传入的。即,外面给了左值,则转化为左值;否则为右值。
因为函数内的参数变量是左值,它可以取地址
-
2. 通用引用 vs 右值引用
通用引用:模版中的参数T&&
和自动推导的auto&&
,则为通用引用
-
若不是标准的模版
T&&
,例如const T&&
,std::vector<T>&&
,都不是通用引用,而是右值引用 -
若通用引用被右值初始化(即传入的是右值),则成为右值引用;否则成为左值引用
通用引用的特性在“引用折叠”中体现。
通用引用T&&
推导时,有下面的规则,这在第6节有用:
-
若传入左值引用,则
T = Type&
-
若传入右值引用,则
T = Type
此外,若定义了
const T&
和T&&
重载,传入右值(临时对象,字面值)会匹配后者。
3. 对右值引用使用std::move
,对通用引用使用std::forward
只对右值引用使用std::move
,而对于通用引用务必使用std::forward
。
若返回右值引用或通用引用,也采用上面的规则。
一些问题:
-
对通用引用使用
std::move
:若传入左值,导致数据被移动走,产生UB -
这种重载场景:一个
const
左值+一个右值,右值对参数使用std::move
,当传入一个字面量时:-
此时传入的参数会生成一个临时拷贝,从而能调用
std::move
,性能不好 -
维护代码多
-
不利于扩展
-
此外,下面这种情况,尽量不要在返回值调用std::move
,误以为这是“优化”:
-
返回函数内局部变量,或某个值参数
-
该局部变量和函数返回值类型相同
上面情况下,编译器会优化(RVO),避免返回值的拷贝。而调用std::move
后,第2个条件就不符合了,无法优化。
-
优化:只会调用一次普通构造函数
-
不优化:会多一次移动构造函数的调用
4. 避免通用引用上的重载
void logAndAdd(const std::string& name) { list.emplace(name); } std::string petName("Darla"); logAndAdd(petName); // 1 logAndAdd(std::string("Persephone")); // 2 logAndAdd("Patty Dog"); // 3
name
传入的是左值,emplace
会有一个拷贝
name
传入的是右值,但它本身是左值,所以emplace
还是有拷贝(可用std::move
执行移动)
name
传入是右值,同2若使用通用引用+
std::forward
,则:
name
传入的是左值,emplace
传入左值,会有一个拷贝
name
传入的是右值,emplace
传入右值,调用std::move
name
传入的是右值且为字面量,emplace
直接从字面量创建std::string
上面例子中,可以看到通用引用的好处。但是若重载它,只有精确匹配类型外,其它都会匹配到通用引用的函数,从而导致错误。
-
例如重载了一个
int
,但传入size_t
等参数,就不会匹配这个重载版本 -
例如重裁了一个父类类型,但传入子类参数,也不会匹配这个重载版本
此外,在构造函数上使用通用引用也不好,也是上述原因,且由于它不影响编译器自动生成的特殊成员函数,因此会和这些函数重载弄混:
-
例如拷贝构造,若传入是
non-const
,则反而会调用通用引用的版本,从而出错 -
容易劫持子类对父类拷贝和移动构造函数的调用(见上面第2条,就是原因)
所以,避免对通用引用重载。
5. 重载通用引用的替代方案
上面说明了,避免重载通用引用。所以需要替代方案。
a. 放弃重载
直接使用其它函数名,就不会有问题。
b. 传递const T&
回退到C++98方案,但这样的效率不如通用引用+std::forward
高。
函数参数就直接用值传递。这在你认为移动比拷贝开销小的时候做。
d. Tag Dispatch
继续使用通用引用,但是通过调用<type_traits>
里的模板,判断T
的类型,然后分发到不同的实现重载中:
-
调用
std::is_integral<typename std::remove_reference_t<T>>()
,判断T
是不是整数,若是则返回一个std::true_type
变量,否则返回std::false_type
变量 -
然后实现的2个重载,一个包含
std::false_type
参数,另一个包含std::true_type
参数
e. 限制使用通用引用的模板
在模板添加额外限制,限制T
的类型,从而避免不必要的匹配和调用。
-
typename = std::enable_if_t<condition>
:当T
符合一定条件时使用 -
里面的
condition
可以用类似std::is_xxx_v<>
使用,例如:std::is_same_v<T, type>
:判断T
和type
是否是一个类型
下面一个例子,只有当T
和R
都为整数时,才能被调用:
template<typename T, typename R,
typename = std::enable_if_t<std::is_integral_v<T> && std::is_integral_v<R>>
>
void call(T&& t, R&& r) {
// ...
}
这里可以实现类似Java泛型
extends
的功能,可使用std::is_base_of_v
此外,在这类模板调用的时候,可以添加static_assert
判断类型是否匹配,从而可以让编译器更清晰地输出错误。
6. 引用折叠
折叠场景:模板推导,auto
推导,typedef
与别名声明,decltype
折叠规则:
-
若中间有左值引用,一律左值(
&+&
,&+&&
,&&+&
) -
否则右值(
&&
,&&+&&
)
例子:std::forward<T>
大体实现:
template<typename T>
T&& forward(std::remove_reference_t<T>& param) {
return std::static_cast<T&&>(param);
}
这里T&&
作为返回值,是通用引用,适用于下面的推导:
-
传入左值,
T = type&
-
传入右值,
T = type
上面的例子,若传入左值,则变为下面的,type& &&
折叠为type&
,为左值:
type& && forward(type& param) {
return std::static_cast<type& &&>(param);
}
若传入右值,则变为下面的,直接返回type&&
,无需折叠,为右值:
type&& forward(type& param) {
return std::static_cast<type&&>(param)
}
Related Issues not found
Please contact @keys961 to initialize the comment
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK