9

Effective Modern C++(6): 右值引用、移动、完美转发(1)

 2 years ago
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.
neoserver,ios ssh client

1. std::movestd::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,误以为这是“优化”:

  1. 返回函数内局部变量,或某个值参数

  2. 该局部变量和函数返回值类型相同

上面情况下,编译器会优化(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
  1. name传入的是左值,emplace会有一个拷贝

  2. name传入的是右值,但它本身是左值,所以emplace还是有拷贝(可用std::move执行移动)

  3. name传入是右值,同2

若使用通用引用+std::forward,则:

  1. name传入的是左值,emplace传入左值,会有一个拷贝

  2. name传入的是右值,emplace传入右值,调用std::move

  3. 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>:判断Ttype是否是一个类型

下面一个例子,只有当TR都为整数时,才能被调用:

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK