C++ 通用引用

C++ 通用引用

&& 并不是在所有情况下都代表右值引用。

Widget&& var1 = someWidget;      // here, “&&” means rvalue reference
 
auto&& var2 = var1;              // here, “&&” does not mean rvalue reference
 
template<typename T>
void f(std::vector<T>&& param);  // here, “&&” means rvalue reference
 
template<typename T>
void f(T&& param);         // here, “&&”does not mean rvalue reference

右值引用只能绑定到右值,而左值引用往往只能绑定到左值,但被声明为 const 的左值引用可以绑定到右值上。

声明为 && 的引用有时既可以绑定到右值上,也可以绑定到左值上,称之为通用引用(Universal References)。由于通用引用常常用于完美转发的场景中,因此又叫又叫转发引用。

🔑 如果一个变量或参数,被声明为 T&& ,T 是某个被推导出的类型,那么这个变量或参数是通用引用

和所有引用一样,通用引用也必须被初始化:

  • 用左值初始化通用引用,它将变为一个左值引用;
  • 用右值初始化通用引用,它将变为一个右值引用

🔑 实践中,可以使用以下法则来区分左值和右值

  • 如果你可以对某个表达式取地址,那么它是左值;
  • 如果一个表达式的类型是左值引用( T& 或 const T& 等),那么它是左值;
  • 否则这个表达式是右值,从概念上来讲,右值代表临时的对象,比如函数的返回值,或通过隐式类型转换创建的值;大多数字面量(比如 10 和 5.3)也是右值

再次考虑之前的代码:

Widget&& var1 = someWidget;
auto&& var2 = var1;

你可以对 var1 取地址,所以 var1 是左值,因此通用引用 var2 被推导为左值引用。

std::vector<int> v;

auto&& val = v[0];   // val becomes an lvalue reference (see below)

由于 v[0] 是左值引用,因此它是左值,通用引用 val 被推导为左值引用。

template<typename T>
void f(T&& param);       // “&&” might mean rvalue reference

f(10);                   // 10 is an rvalue

10 是右值,因此 param 被推导为右值引用。

int x = 10;
f(x);                        // x is an lvalue

x 是左值,因此 param 被推导为左值引用。

🔑 只有 && 跟类型推导一起出现时,它才是一个通用引用

通用引用必须以 T&& 的形式出现,虽然 std::vector<T>&& param 中 T 的类型会被推导,但它是右值引用:

template<typename T>
void f(std::vector<T>&& param);     // “&&” means rvalue reference

即使在前面加上 const 也会让 && 不再被解释为通用引用:

template<typename T>
void f(const T&& param); // “&&” means rvalue reference

但有时,即使形式为 T&& 的引用也不是通用引用:

template <class T, class Allocator = allocator<T> >
class vector {
public:
    ...
    void push_back(T&& x);       
	// fully specified parameter type ⇒ no type deduction;
    ...                          // && ≡ rvalue reference
};

因为 T 的类型是由 vector<T> 的模板参数 T 决定的,这里并不存在类型推导,因此它不是通用引用。

与 emplace_back 的参数作比较,它的类型独立于 vector<T> 的模板参数 T,是编译器从实参中推导出来的,因此它是通用引用:

template <class T, class Allocator = allocator<T> >
class vector {
public:
    ...
    template <class... Args>
    void emplace_back(Args&&... args); 
	// deduced parameter types ⇒ type deduction;
    ...                       // && ≡ universal references
};

引用折叠

模板实参推断

函数参数是通用引用时:

  • 传递给它左值,编译器推断 T 类型为实参的左值引用类型
  • 传递给它右值,编译器推断 T 类型为实参的原始类型
    • 注意,右值并不会被推导为右值引用类型

因此,使用通用引用时,可能间接产生引用的引用(注意,在 C++中直接声明引用的引用是非法的),这种情况下会发生引用折叠:

  • X& &X& &&X&& & 都折叠为 X&
  • X&& && 折叠为 X&&
template <typename T> void func(T &&t);

int main() {
  int i = 0;
  int& ir1 = i;
  int&& ir2 = 10;
  const int ci = 100;
  const int &cir = ci;
  func(i);            // void func<int &>(int &t)
  func(ir1);          // void func<int &>(int &t)
  func(ir2);          // void func<int &>(int &t)
  func(std::move(i)); // void func<int>(int &&t)
  func(ci);           // void func<const int &>(const int &t)
  func(cir);          // void func<const int &>(const int &t)
}

传 ir1 进去,T 被推断为 int &,所以参数类型为 int & &&,根据引用折叠的规则,它被折叠为 int &,因此,通用引用可以通过类型推导和引用折叠机制变为左值引用。

如果传给通用引用的初始值本身是左值引用或右值引用,则它的引用性质被忽略,因此传给 T&& 参数一个 int&int&&,都会被看作 int,ir1 和 ir2 都是左值,所以 T 都会被推导为 int &

总结一下,类型推导机制区分实参是左值还是右值,引用折叠机制来将推导出的引用变成一个“正常”的引用,因此,可以将通用引用理解为引用折叠上下文中的右值引用。

将左值转换为右值的常规方法是调用 std::move

Widget var2 = std::move(var1);             // equivalent to above

事实上与使用 static_cast<Widget&&>(var1) 做转换的效果是相同的。

我们以 std::move 的实现为例,讲解类型推导的过程:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    return static_cast<typename remove_reference<T>::type&&>(t);
}

对于表达式 std::move(string("hello"))

  • 传进来的是右值,所以 T 被推导为 string
  • t 的类型为 string &&
  • remove_reference<T>::type&&string &&

对于表达式 s1 = string("world"); std::move(s1);

  • 传进来的是左值,所以 T 被推导为 string&
  • t 的类型为 string& &&,被折叠为 string&
  • remove_reference<T>::type&&string &&

所以无论实参的类型是左值还是右值,std::move 都会将它转换为右值

auto

引用折叠也会发生在用 auto&& 声明变量的上下文中,auto 的推导规则和模板实参推导基本相同(仅在列表初始化的上下文中稍有区别)。

Widget&& var1 = someWidget;      // var1 is of type Widget&& (no use of auto here)
 
auto&& var2 = var1;              // var2 is of type Widget& (see below)

var1 是左值,因此推断出的类型为 Widget&,根据引用折叠,初始化语句相当于:

Widget& var2 = var1;             // var2 is of type Widget&

typedef

引用折叠可能发生的第三个场景是 typedef。

template<typename T>
class Widget {
    typedef T& LvalueRefType;
    ...
};

当我们这样使用该模板时:

Widget<int&> w;

typedef 语句变为:

typedef int& & LvalueRefType;

此时引用折叠发生,产生代码:

typedef int& LvalueRefType;

我们在引用的场景下使用该 typedef 类型也会发生引用折叠:

void f(Widget<int&>::LvalueRefType&& param);

param 的类型相当于:

void f(int& && param);

发生引用折叠,变为:

void f(int& param);

decltype

最后一个可能发生引用折叠的场景为 decltype,decltype 上应用的类型推导规则不同于模板和 auto,需要注意的是,它的类型推导仅依赖于 decltype 表达式,与初始值无关,因此

Widget w1, w2;
 
auto&& v1 = w1;		// v1 的类型为 Widget&       
 
decltype(w1)&& v2 = w2;	// v2 的类型是 Widget&&,而 w2 是左值
			// 因此该语句是非法的

通用引用和函数重载

通用引用几乎可以精确匹配任何类型的实参,因此有通用引用重载的情况下,函数匹配规则会变得异常的复杂:

对此,Effective Modern C++ 给我们的建议是不要在通用引用上做函数重载。

🔑 Effective Modern C++, Item 26: Avoid overloading on universal references (Scott Meyers)

参考资料

posted @ 2022-02-26 23:04  路过的摸鱼侠  阅读(547)  评论(0编辑  收藏  举报