使用前进的优势

南笙酒味 提交于 2020-02-27 20:07:17

在理想的转发中, std::forward用于将命名的右值引用t1t2转换为未命名的右值引用。 这样做的目的是什么? 如果将t1t2保留为左值,这将如何影响被调用函数的inner

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

#1楼

我认为有一个实现std :: forward的概念代码可以增加讨论的范围。 这是斯科特·迈耶斯(Scott Meyers)讲的《有效的C ++ 11/14采样器》中的一张幻灯片

代码中的函数movestd::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。


#2楼

尚未明确的一点是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)”,则此方法无效。


#3楼

如果将t1和t2保留为左值,这将如何影响被调用函数的内部?

如果在实例化之后, T1char类型,而T2是类,则您希望传递每个副本t1和每个const引用t2 。 好吧,除非inner()每个非const引用都接受它们,也就是说,在这种情况下,您也要这样做。

尝试编写一组outer()函数,该函数在没有右值引用的情况下实现此功能,从而推断出从inner()类型传递参数的正确方法。 我认为您将需要2 ^ 2的东西,需要大量的模板元数据来推论参数,并且需要大量时间来在所有情况下都做到这一点。

然后有人带来了inner() ,它每个指针都接受参数。 我认为现在等于3 ^ 2。 (或4 ^ 2。该死,我不必费心去考虑const指针是否会有所作为。)

然后想象您要为五个参数执行此操作。 或七个。

现在您知道了为什么有一些聪明的想法想出“完美转发”的方法:它使编译器为您完成所有这些工作。


#4楼

您必须了解转发问题。 您可以详细阅读整个问题 ,但我将进行总结。

基本上,给定表达式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-references,但是const_castconst移开:

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!

最终的解决方案可以正确处理所有问题,但要以无法维护为代价。 使用const和non-const的所有组合提供f重载:

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函数内部,并且已经传递了一个左值。 这意味着TA& ,因此静态类型转换的目标类型是A& && ,或者仅仅是A& 。 由于x已经是A& ,所以我们什么也不做,只剩下一个左值引用。

当我们传递了一个右值时, TA ,因此静态类型转换的目标类型为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);

#5楼

在理想的转发中,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是整数文字,并且是这样的右值。 但是由于表达式inner(t1,t2);中的t1t2 inner(t1,t2); 是左值,则您将调用#1而不是#2。 这就是为什么我们需要使用std::forward将引用转换回未命名的引用。 因此, outer t1始终是左值表达式,而forward<T1>(t1)可能是取决于T1的右值表达式。 如果T1是左值引用,则后者只是左值表达式。 并且,如果对external的第一个参数是左值表达式,则仅推导T1为左值引用。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!