Featured image of post C++:std::forward的正确打开方式

C++:std::forward的正确打开方式

把群友聊了很多次的话题拿来当作自己的知识储备😤

C++:std::forward的正确打开方式

起因是某天在群友聊天时有人问到了这个问题:

然后回答是:

cppreference

cppreference

再往前翻翻发现有人一周前就问过 (看来确实是日经问题了)

伟大的y神站了出来

链接

看到这里,其实问题本身应该已经解决了,但是我发现自己对“完美转发”,“左值”和“左值引用”,“右值”和“右值引用”的概念还是不够清晰,故写一篇文章来总结一下。

参考了一下这篇博客中的内容。

1. 前置基础知识

1.1 引用折叠

关于引用折叠, 用书面一点的术语表达来说, 只需要记住一种情况:

右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。

也即:

  • T&& && 折叠成 T&&
  • -(分割线)-
  • T& & 折叠成 T&
  • T&& & 折叠成 T&
  • T& && 折叠成 T&

基于此, 我们能写出这样的代码:

template<typename T1, T2, T3>
void f(T1 &&arg1, T2 &&arg2, T3 &&arg3){
    // ...
}

由于模板参数可以直接指代带引用的类型,所以在模板实参匹配时,可能出现“引用的引用”情况。根据引用折叠,实际参数的引用类型将取决于实参的引用类型。即:这段代码对于每一个参数来说,都可以任意匹配它的左值和右值。

这种引用,就被称为"万能引用"

1.2 区分"类型"和"值类别"

在匹配时, 还需要区分表达式的类型值类别

例如以下代码:

void f(T&& arg) {
    arg = //...
}

这里 arg的类型是“对 T的右值引用”,而 arg本身在 f()内部是左值!!!因为很显然,实参在函数调用栈帧中有地址。所以上述代码是可以运行的,虽然 arg是右值引用类型,但我们可以给它赋值。

所以,在万能引用中,每个参数的“值类别”都是左值,但我们得想办法搞清楚它们的“类型”是左值引用还是右值引用。

2. 模板实参推导

回到问题本身,何谓模板实参推导?

《C++ Primer》里的相关章节:

模板实参推导是指编译器使用函数调用中的实参来推断模板实参的过程。

16.2.5 模板实参推断和引用(608页)

为了理解如何从函数调用进行类型推断,考虑下面的例子:

template <typename T> 
void f(T &p);

从左值引用函数参数推断类型

当一个函数参数是模板类型参数的一个普通(左值)引用时(即,形如T&的参数),绑定规则告诉我们,只能传递给它一个左值(如,一个变量或一个返回引用类型的表达式)。实参可以是const类型,也可以不是。如果实参是const的,则T被推断为const类型。

template <typename T> void f1(T &p); // 实参必须是一个左值
// 对f1的调用使用实参所引用的类型作为模板参数类型
f1(i); // i是一个int,因此T是int
f1(42); // 错误:传递给一个&参数的实参必须是一个左值

如果一个函数参数的类型是const T&,正常的绑定规则告诉我们可以传递它任何类型的实参—一个对象(const或非const),一个临时对象或是一个字面常量值。当函数参数本身是const时,T的类型推断的结果不会是一个const类型。const已经是函数参数类型的一部分; 因此,它不会也是模板参数类型的一部分。

在不省略形参名字的情况下可以从ide的静态检查看得更清楚

从右值引用函数参数推断类型

当一个函数参数是一个右值引用时(即,形如T&&的参数),正常的绑定规则告诉我们可以传递给它一个右值, 当我们这样做时,类型推断过程类似普通左值引用函数参数的推断过程。推断出的T的类型是该右值实参的类型:

template <typename T> void f3(T &&p);
f3(42); // 实参是一个int类型的右值; 模板参数T被推断为int
Licensed under CC BY-NC-SA 4.0
最后更新于 Mar 26, 2024 18:03 CST