万能引用(Universal Reference),引用折叠(Reference Collapsing)与完美转发(Perfect Forwarding)

万能引用(Universal Reference),引用折叠(Reference Collapsing)与完美转发(Perfect Forwarding)

[TOC]

本文简单介绍万能引用(Universal Reference), 引用折叠(Reference Collapsing)与完美转发(Perfect Forwarding)的相关内容.

引言

前面的博文介绍了右值, 右值引用以及移动语义的理解. 其中有一个问题是 std::move() 如何实现入参是左值引用的情况下依旧能通过编译. 下面是 std::move 的原型. 这里需要用到 万能引用 Universal Reference 的概念.

1
2
template <typename T>
typename remove_reference<T>::type&& move(T&& t)

再考虑另一个问题, 既然在博文中介绍的拷贝构造函数与移动构造函数可以用左值以及右值的区别实现重载, 那遇到模板编程会怎样呢? 毕竟在 C++ 中, 不存在引用的引用, 因此A& &的写法是无法编译的. 这里需要介绍引用折叠的规则(Reference Collapsing Rules).

万能引用

引入

缩写:

左值引用: LRef = Lvalue reference
右值引用: RRef = Rvalue reference
万能引用: URef = Universal reference

注意此处的万能引用的概念其实是 Scotter Meyers 自己创造的概念. 用来方便理解规则.

结论: type && ⇏ RRef

更具体一些:

  • RRef ⇒ type&&
  • type&& ⇏ RRef

也就是说右值引用一定是 && 类型, 然而 && 类型却不一定对应右值引用(充分不必要条件), 也可能对应左值引用.

因为&& 类型可以代表 2 种引用, 包括另一种引用类型 URef.

  1. RRef
    • 只绑定右值
    • 方便移动语义
  2. URef
    • 既可以表示左值引用也可以表示右值引用.
    • 既可以绑定左值也可以绑定右值, 既可以绑定 const 也可以绑定 non-const, 也就是说可以绑定所有类型

定义与规则

If a variable or parameter has declared type T&& for some deduced type T, it’s a universal reference.

只要满足条件1: 变量被声明为 && 类型, 条件2: 需要被推断类型. 那么它就是万能引用 URef.

规则如下:

  • 当 URef 的初始值是 T 类型的 lvalue 时, URef 变成 LRef, T&.

  • 当 URef 的初始值是 T 类型的 rvalue 时, URef 变成 RRef, T&&.

应用场景:

  • 函数的模板参数,形式如下:

    1
    2
    template<typename T>
    void f(T&& param);
  • auto 声明

    1
    auto&& var = ... ;
  • typedef声明

  • decltype声明

例子

  1. 模板参数

    1
    2
    3
    4
    5
    6
    template<typename T>
    void f(T&& param); // URef: proper syntax + deduced type
    Widget w;
    f(w);// w 是 lvalue ⇒ URef 成为 LRef;因此得到f(Widget&)
    f(std::move(w));// std::move 产生 rvalue ⇒ URef 成为 RRef; 因此得到 f(Widget&&)
    f(Widget());// Widget() 产生 rvalue ⇒URef 成为 RRef; 因此得到 f(Widget&&)
  2. auto声明

    1
    2
    3
    4
    5
    std::vector<int> v;
    ...
    auto&& val = 10;// 10 是 rvalue ⇒ URef 成为 RRef;val 的类型是 int&&
    auto&& element = v[5];// v[5] 返回类型为 int& 且 LRefs 是 左值 ⇒ v[5] 是 lvalue ⇒ URef 成为 LRef;
    //因此 element’s type is int&

不是所有的 && 类型都是万能引用

区别点在于类型的推断是否真正在进行, 例如 STL 中的 std::vector, push_back() 函数中的 T 模板参数类型来自于 vector<T> 根本没有参数传入到 push_back() 进行推导, 因此不是 URef.

1
2
3
4
5
6
7
8
template<class T,
class Allocator=allocator<T>>
class vector {
public:
...
void push_back(T&& x);// RRef! T comes from vector<T>,not arg passed to push_back
...
};

与之相对的是 emplace_back(), 有参数 Args&&... args 传入, 需要进行推导, 产生的即为 Uref.

1
2
3
4
5
6
7
8
9
template<class T,
class Allocator=allocator<T>>
class vector {
public:
...
template<class... Args>
void emplace_back(Args&&... args);// URef! Args deduced
...
};

Uref 相关的重载

重载 URef 毫无意义, 因为 URef 本来就可以用所有的类型初始化, 无法区分重载, 并且还会导致误解.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MessedUp {
public:
template<typename T>
// 本想应用到所有的 lvale,结果只能应用到 const lvale
void doWork(const T& param);
template<typename T>
// 本想应用到所有的 lvale,结果可以应用到所有类型(除了上面的 const lvale)
void doWork(T&& param);
};
MessedUp m;
Widget w;
const Widget cw;
m.doWork(w);// doWork(T&&)
m.doWork(std::move(w));// doWork(T&&)
m.doWork(cw);// doWork(const T&)
m.doWork(std::move(cw));// doWork(T&&)

//对于非成员函数也是如此
template<typename T>
void doWork(const T& param);
template<typename T>
void doWork(T&& param);
Widget w;
const Widget cw;
doWork(w);// doWork(T&&)
doWork(std::move(w));// doWork(T&&)
doWork(cw);// doWork(const T&)
doWork(std::move(cw));// doWork(T&&)

因此有别于 LRef 与 RRef 的差异性可以导致重载, URef 需要使用完美转发 std::forward 实现输入参数的类型区分. 完美转发稍后展开.

1
2
3
4
5
6
7
8
9
10
11
12
13
//LRef 与 RRef
void doWork(const Widget& param){
ops and exprs using param // copy
}
void doWork(Widget&& param){
ops and exprs using std::move(param) // move
}

//URef
template<typename T>
void doWork(T&& param){
ops and exprs using std::forward<T>(param)
}

URef 书写上的好习惯

尽量使用 ParamType的名字取代 T, 同时在 && 前加上空格, 方便阅读代码.

1
2
3
template<typename ParamType>
void f(ParamType && param);
auto && I_did_it_my_way = 44;

引用折叠

一般场景

对于不是万能引用的场景(例如不使用模板参数的函数), 我们依旧可能会遇到”引用的引用”, 怎么破? C++ 制定了引用折叠的规则. 如下:

  • T& & ⇒ T&
  • T&& & ⇒ T&
  • T& && ⇒ T&
  • T&& && ⇒ T&&

简单的来说:

  • RRef-to-RRef ⇒ RRef 只有右值引用-右值引用的情况下才会推导出结果为右值引用
  • LRef-to-anything ⇒ LRef 其他时候都是推导为左值引用(可以用 infectious , 传染性的来形容 LRef 的效果)

但是需要注意的是万能引用里的规则与引用折叠并不是对立的, 尤其是在模板参数推断等情形, 如下面的例子在上面万能引用的例子中也被使用. 所以看了许久我也是很疑惑, 有时候需要跳过这些概念(感觉这里的概念是起源于现象, 而不是先有抽象的设计再制定的规则).

  • 模板参数
1
2
3
4
5
template<typename T>
void f(T&& param); // acts like a URef, is an RRef
Widget w;
f(w); // f(Widget& &&) ⇒ f(Widget&)
f(std::move(w)); // f(Widget&&)
  • auto 声明

    1
    2
    3
    Widget w;
    auto&& v1 = w; // lvalue initializer, v1’s type is Widget&
    auto&& v2 = std::move(w); // rvalue initializer, v2’s type is Widget&&
  • typedef 声明

1
2
3
4
5
6
7
8
9
10
template<typename T>
class Widget {
typedef T& LvalueRefType;
...
};
Widget<int&> w; // Widget<int&>::LvalueRefType is int& & ⇒ int&
typedef Widget&& RRtoW;
RRtoW& v1 = w; // v1’s type is Widget&
const RRtoW& v2 = std::move(w); // v2’s type is const Widget&
RRtoW&& v3 = std::move(w); // v3’s type is Widget&&
  • declared 声明
    decltype 的规则与上面有些不同, 这里挖个坑, 后面整理一下.

变量本身为引用类型时, 需要去除引用外衣

当变量本身为引用类型时, 需要去除引用, 只保留本身类型. 如下 lrw 左值引用, rrw 为右值引用, 但是传入函数 f() 之前会都变成 Widget 类型, 都是左值, 然后按照万能引用的规则(左值初始化为左值引用)可得 f() 的参数实际为左值引用. 除非显式约束参数为右值(使用 std::move ).

1
2
3
4
5
6
7
8
9
template<typename T>
void f(T&& param);
Widget w;
...
Widget& lrw = w; // lrw’s type is Widget&
Widget&& rrw = std::move(w); // rrw’s type is Widget&&
f(lrw); // f<Widget&>(Widget&)
f(rrw); // f<Widget&>(Widget&)
f(std::move(rrw)); // f<Widget>(Widget&&)

小结

type && ⇏ RRef.
type && + 类型推断 ⇒ 万能引用 URef.
URef 依据初始值转化为 LRef 或者 RRef. Lvalue ⇒ LRef, Rvalue ⇒ RRef.
不是所有的 T&&s 模板参数都是 URef.一定要注意判断是不是真的有类型推断.
不要去重载 URef.
类型推断的基础是引用折叠.
以上概念的应用场景为:function templates, auto, typedef, and decltype.

完美转发

由于 && 不一定意味着右值引用, 如果我们的本意是希望传入右值引用, 结果折叠/万能引用等规则转化成了左值引用, 那有什么办法解决吗?分为 2 种情况考虑.

非模板函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class shape{}

void foo(const shape&)
{
puts("foo(const shape&)");
}
void foo(shape&&)
{
puts("foo(shape&&)");
}
void bar(const shape& s)
{
puts("bar(const shape&)");
foo(s);
}
void bar(shape&& s)
{
puts("bar(shape&&)");
foo(s);
}
int main()
{
bar(shape());
}

输出为:

1
2
bar(shape&&)
foo(const shape&)

shape() 返回的是一个右值, 因此调用的 bar() 的重载函数是 void bar(shape&& s). 但是传入到内部的 foo() 的参数 s 是左值,因此调用的重载函数是 void foo(const shape&). 这与我们想调用 void foo(shape&&) 的本意不符.

解决办法是显示地强制转化为右值, 使用 std::move 或者是类型转换 static_cast.

1
2
3
foo(std::move(s));
//OR
foo(static_cast<shape&&>(s));

模板函数

对于模板函数, 我们可以使用完美转发 std::forward 解决这个问题.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void foo(const shape&)
{
puts("foo(const shape&)");
}
void foo(shape&&)
{
puts("foo(shape&&)");
}
template <typename T>
void bar(T&& s)
{
foo(std::forward<T>(s));
}
int main() {
circle temp;
bar(temp);
bar(shape());
}

输出为:

1
2
foo(const shape&)
foo(shape&&)

std::forwardstd::move 的源码解析

首先看依赖的 strcut remove_reference, 它的作用如其名, 删除引用类型返回实体类型 type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//非引用类型直接返回本身类型即可
template<typename _Tp>
struct remove_reference
{ typedef _Tp type; };

// 左值引用
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp type; };

// 右值引用
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp type; };

然后是 std::forward,下面是对左值引用的转发

1
2
3
4
5
6
7
8
9
10
/**
* @brief Forward an lvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }

如果输入为左值引用, 由于返回为右值引用(通过 static_cast 强制转换), 所以 Tp& && 发生引用折叠返回结果为 Tp & 仍旧为左值引用.
如果输入为右值引用, Tp&& &&折叠结果仍旧为 Tp &&, 仍旧为右值引用.

对右值引用的转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @brief Forward an rvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}

先判断传入参数为右值才往下进行, 然后 Tp&& && 折叠结果仍旧为 Tp &&, 仍旧为右值引用.
其中 std::is_lvalue_reference<_Tp> 是一个定义在 type_traits 中的模板函数用来判断是否是右值引用.

最后看一下 std::move 的源码.

1
2
3
4
5
6
7
8
9
/**
* @brief Convert a value to an rvalue.
* @param __t A thing of arbitrary type.
* @return The parameter cast to an rvalue-reference to allow moving it.
*/
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

很直接返回 Tp + &&.

参考链接/材料:
https://cloud.tencent.com/developer/article/1561681

https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers “Universal References in C++11”,Scott Meyers,2012

万能引用(Universal Reference),引用折叠(Reference Collapsing)与完美转发(Perfect Forwarding)

https://www.chuxin911.com/c++_uref_forwarding_20220105/

作者

cx

发布于

2022-01-05

更新于

2022-07-19

许可协议