Answers:
您必须了解转发问题。您可以详细阅读整个问题,但我将进行总结。
基本上,给定表达式E(a, b, ... , c)
,我们希望表达式f(a, b, ... , c)
是等效的。在C ++ 03中,这是不可能的。尝试了很多,但都在某些方面失败了。
最简单的方法是使用左值引用:
template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
E(a, b, c);
}
但这无法处理临时值:f(1, 2, 3);
,因为它们不能绑定到左值引用。
下一个尝试可能是:
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
E(a, b, c);
}
可以解决上述问题,但会出现触发器。现在,它不允许E
包含非常量参数:
int i = 1, j = 2, k = 3;
void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these
第三次尝试接受常量引用,但随后const_cast
的的const
路程:
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}
这可以接受所有值,可以传递所有值,但可能导致未定义的行为:
const int i = 1, j = 2, k = 3;
E(int&, int&, int&); f(i, j, k); // ouch! E can modify a const object!
最终的解决方案可以正确处理所有问题,但要以无法维护为代价。您提供的重载f
,以及const和non-const的所有组合:
template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);
template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);
template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);
template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);
template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);
template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);
template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);
N个参数需要2 N个组合,这是一场噩梦。我们希望自动执行此操作。
(这实际上是使编译器在C ++ 11中为我们完成的工作。)
在C ++ 11中,我们有机会解决此问题。一种解决方案修改了现有类型的模板推导规则,但这可能会破坏大量代码。因此,我们必须寻找另一种方法。
解决方案是改为使用新添加的rvalue-references;我们可以在推导右值引用类型并创建任何所需结果时引入新规则。毕竟,我们现在不可能破坏代码。
如果给一个参考基准(附注引用是一个包罗万象的术语,意思都T&
和T&&
),我们使用以下规则计算出结果类型:
“给定类型T的引用类型T,尝试创建类型“对cv TR的左值引用”会创建类型“对T的左值引用”,而尝试创建对类型T的“右值引用” cv TR”创建TR类型。”
或以表格形式:
TR R
T& & -> T& // lvalue reference to cv TR -> lvalue reference to T
T& && -> T& // rvalue reference to cv TR -> TR (lvalue reference to T)
T&& & -> T& // lvalue reference to cv TR -> lvalue reference to T
T&& && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)
接下来,使用模板参数推导:如果参数是左值A,则为模板参数提供对A的左值引用。否则,我们通常进行推论。这提供了所谓的通用参考(术语“ 转发参考”现在是正式参考)。
为什么这有用?因为结合起来,我们可以保持跟踪类型的值类别的能力:如果是左值,则有一个左值引用参数,否则有一个右值引用参数。
在代码中:
template <typename T>
void deduce(T&& x);
int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)
最后一件事是“转发”变量的值类别。请记住,一旦在函数内部,该参数就可以作为左值传递给任何对象:
void foo(int&);
template <typename T>
void deduce(T&& x)
{
foo(x); // fine, foo can refer to x
}
deduce(1); // okay, foo operates on x which has a value of 1
不好 E需要获得与我们得到的相同的价值类别!解决方法是这样的:
static_cast<T&&>(x);
这是做什么的?考虑我们在deduce
函数内部,并且已经传递了一个左值。这表示T
为A&
,因此静态类型转换的目标类型为A& &&
或A&
。由于x
已经是A&
,我们什么也不做,只剩下一个左值引用。
当我们传递了一个右值T
is时A
,因此静态类型转换的目标类型为A&&
。强制转换会产生一个右值表达式,该表达式不再可以传递给左值引用。我们维护了参数的值类别。
将它们放在一起可以使我们“完美转发”:
template <typename A>
void f(A&& a)
{
E(static_cast<A&&>(a));
}
当f
收到一个左值时,E
获取一个左值。当f
接收到一个右值,E
得到一个右值。完善。
当然,我们要摆脱丑陋的境地。static_cast<T&&>
晦涩难懂 相反,让我们创建一个名为的实用程序函数forward
,该函数执行相同的操作:
std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
const int i
将被接受:A
被推导为const int
。失败是针对右值文字的。还要注意,对于deduced(1)
,x是int&&
,而不是int
(完美的转发永远不会复制,就像x
将其作为按值参数一样)。仅仅T
是int
。x
在转发器中求值为左值的原因是因为命名的右值引用成为左值表达式。
forward
或move
此处有什么区别吗?还是仅仅是语义上的差异?
std::move
应在没有显式模板参数的情况下调用,并且始终会导致右值,而std::forward
最终可能会出现右值。使用std::move
时,你知道你不再需要的价值,并希望到别处移动它,用std::forward
做根据传递给你的函数模板中的值。
我认为有一个实现std :: forward的概念代码可以增加讨论的范围。这是斯科特·迈耶斯(Scott Meyers)讲的《有效的C ++ 11/14采样器》中的一张幻灯片
move
代码中的功能是std::move
。在该演讲的前面有一个(有效的)实现。我在libstdc ++中的move.h文件中找到了std :: forward的实际实现,但是这完全没有启发性。
从用户的角度来看,其含义std::forward
是有条件地强制转换为右值。如果我编写的函数期望参数中包含左值或右值,并且仅当将其作为右值传递时,希望将其作为右值传递给另一个函数,则该功能很有用。如果我没有将参数包装在std :: forward中,它将始终作为常规引用传递。
#include <iostream>
#include <string>
#include <utility>
void overloaded_function(std::string& param) {
std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
std::cout << "std::string&& version" << std::endl;
}
template<typename T>
void pass_through(T&& param) {
overloaded_function(std::forward<T>(param));
}
int main() {
std::string pes;
pass_through(pes);
pass_through(std::move(pes));
}
果然,它会打印
std::string& version
std::string&& version
该代码基于前面提到的演讲中的示例。从大约开始15:00开始滑动10。
在理想的转发中,std :: forward用于将命名的右值引用t1和t2转换为未命名的右值引用。这样做的目的是什么?如果将t1和t2保留为左值,对被调用函数内部的影响如何?
template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) { inner(std::forward<T1>(t1), std::forward<T2>(t2)); }
如果在表达式中使用命名的右值引用,则它实际上是左值(因为您按名称引用对象)。考虑以下示例:
void inner(int &, int &); // #1
void inner(int &&, int &&); // #2
现在,如果我们称outer
这样的
outer(17,29);
我们希望将17和29转发给#2,因为17和29是整数文字,并且是这样的右值。但是由于表达式中的t1
和是左值,因此您将调用#1而不是#2。因此,我们需要使用将引用转换回未命名的引用。因此,in 始终是一个左值表达式,而取决于则可能是一个右值表达式。后者仅是左值表达式,如果t2
inner(t1,t2);
std::forward
t1
outer
forward<T1>(t1)
T1
T1
是左值引用。并且T1
只有在external的第一个参数是左值表达式的情况下,才推断出它是左值引用。
如果将t1和t2保留为左值,这将如何影响被调用函数的内部?
如果,实例化后,T1
是类型的char
,并且T2
是一类的,你要传递t1
每复制和t2
每const
参考。好吧,除非inner()
每个非const
引用都采用它们,也就是说,在这种情况下,您也要这样做。
尝试编写一组在outer()
没有右值引用的情况下实现此功能的函数,从而推断出从中传递参数的正确方法inner()
的类型。我认为您将需要2 ^ 2的东西,可以使用大量的模板元数据来推论参数,并且需要大量时间才能在所有情况下都做到这一点。
然后有人来了inner()
,每个指针都接受参数。我认为现在等于3 ^ 2。(或4 ^ 2。该死,我不必费心去思考const
指针是否会有所作为。)
然后想象您要为五个参数执行此操作。或七个。
现在您知道了为什么有一些聪明的想法想出“完美转发”的方法:它使编译器为您完成所有这些工作。
还没有明确说明的一点是也可以正确static_cast<T&&>
处理const T&
。
程序:
#include <iostream>
using namespace std;
void g(const int&)
{
cout << "const int&\n";
}
void g(int&)
{
cout << "int&\n";
}
void g(int&&)
{
cout << "int&&\n";
}
template <typename T>
void f(T&& a)
{
g(static_cast<T&&>(a));
}
int main()
{
cout << "f(1)\n";
f(1);
int a = 2;
cout << "f(a)\n";
f(a);
const int b = 3;
cout << "f(const b)\n";
f(b);
cout << "f(a * b)\n";
f(a * b);
}
产生:
f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&
注意,“ f”必须是模板函数。如果仅将其定义为“ void f(int && a)”,则此方法无效。
可能值得强调的是,转发必须与带有转发/通用引用的外部方法一起使用。允许将自己单独使用forward作为以下语句,但是除了引起混乱之外,它没有其他好处。标准委员会可能希望禁用这种灵活性,否则我们为什么不只使用static_cast呢?
std::forward<int>(1);
std::forward<std::string>("Hello");
我认为,前进和前进是设计模式,是引入r值参考类型后的自然结果。除非禁止不正确的用法,否则我们不应以正确使用的方法命名。
f
是函数,而不是表达式?