使用 std::forward with auto&& 是正确的做法

Is use of std::forward with auto&& the right thing to do

本文关键字:forward with auto 使用 std      更新时间:2023-10-16

试图了解将std::forwardauto&&变量一起使用是否是传递这些变量以允许移动的正确方法。

假设有一个函数:

void moveWidget(Widget&& w);

调用方-两个变量分别表示右值和左值:

Widget w;
auto&& uniRefLV = w;            // lvalue initialiser, 
                                // uniRefLV's type is Widget&
auto&& uniRefRV = std::move(w); // rvalue initialiser, 
                                // uniRefRV's type is Widget&&

我们知道auto&&类型的变量是通用引用,因为存在类型推导。这意味着uniRefRVuniRefLV都是通用引用

在我的例子中,很明显uniRefRV右值uniRefLV左值用引用。如果定义不同,它们可以表示左值值

现在,我想调用moveWidget()并完善这些通用引用类型。该指南(Scott Meyers)说:

通过std::move传递并返回右值引用,通过std::forward传递并返回通用引用

除非我完全误解了指南,否则使用std::forward似乎是合乎逻辑的。但让我们考虑所有可能的选择:

// (1) std::move:
moveWidget(std::move(uniRefLV)); // Compiles and looks fine
                                 // but violates the guideline?
                                 // (unconditionally casts lvalue to rvalue)
moveWidget(std::move(uniRefRV)); // Same as above - but not an issue here
                                 // as we cast rvalue to rvalue
// (2) std::forward with Widget:
moveWidget(std::forward<Widget>(uniRefLV)); // Compiles, follows the guideline
                                            // but doesn't look right - what if
                                            // we didn't know Widget's type?
moveWidget(std::forward<Widget>(uniRefRV)); // Same as above
// (3) std::forward with decltype:
moveWidget(std::forward<decltype(uniRefLV)>(uniRefLV)); // Fails to compile! (VC10)
                                                        // follows the guideline
                                                        // has nice and short syntax :)
moveWidget(std::forward<decltype(uniRefRV)>(uniRefRV)); // Compiles fine

你认为我们应该平等对待引用uniRefLVuniRefRV吗?我们应该使用三个选项中的哪一个来实现完美转发?

您误解了指南。或者至少过于字面化了。

我认为这里有三件重要的事情需要认识。

首先,所有类型在这里都是已知的。通用引用的建议主要适用于带有模板的泛型代码,在这些模板中,您根本不知道某个东西是或接受了左值引用还是右值引用。

其次,函数接受一个右值引用:您必须传递一个右价。时期这里别无选择。

逻辑结论是,你不想传递一个通用引用:无论你传递什么,都必须是右值,它永远不可能是左值。通用引用可以是左值(如果它们被实例化为左值引用)。传递一个通用引用意味着"我不知道这是什么样的引用,我可以将其作为右值或左值传递,所以我传递它的方式与我得到的完全一样"。这个问题的情况更像是"我确切地知道我必须通过什么,所以这就是我将要通过的"。

让我们假设这个案例并不像看上去那么简单。代替giveMeInt,我们有这样的东西:

template <typename T>
typename complex_computation<T>::type giveMeSomething(T t);

而不是moveMe,它实际上具有通用引用,因此不需要无条件的std::move

template <typename T>
void wantForwarded(T&&);

现在您实际上需要完美的转发。

auto&& uniRef = giveMeSomething(iDontKnowTheTypeOfThis);
wantForwarded(std::forward<decltype(uniRef)>(uniRef);

我觉得这里有点混乱。uniRefLVuniRefRV都不是"通用参考文献"。它们只是变量,都有一个确定的类型。这两种类型恰好都是引用类型,但这并不重要。

要调用moveWidget,在这两种情况下都必须使用std::move,并且在这两个情况下,语义都是在移动之后,原始的w已经从中移动,因此不再处于确定状态。(你能做的最好的事情就是销毁它或重新分配给它。)

现在,相比之下,真正的通用引用模板参数,并且它必须始终是规范形式:

template <typename T> void foo(T &&);
//                            ^^^^^^
foo(bar());
foo(x);
foo(std::move(y));

也就是说,

  • 模板是函数模板
  • 模板参数T作为函数参数T &&出现,并且
  • 实现专业化的模板论证是推导出的

当满足这些条件时,T &&是通用引用,您可以通过std::forward<T>传递它,从而保留正确的引用类型。

(1).a这并不违反准则。你特别说"我不再需要那个左值了,所以接受它"这基本上就是std::move的作用。如果您在完成std::继续使用uniRefLV,这是违反准则的。该值(可能)已消失,您将其忽略。斯科特就是这么说的。

(1).b这是合法的。虽然std::move在那里是不需要的,但它确实简化了读取源代码。

(2),(3)我们不必为std::forward提供模板参数。它将由编译器推导出来!至少我的理解是这样的。我认为手动操作没有意义。Forward只是从类型定义中移除的引用(&&和&)。这就是为什么它被称为转发并用于数据转发。当您有功能时:样板void foo(T&&T);

它可以实例化为以下任一项:

void foo(SomeType & t);
void foo(SomeType && t);
void foo(const SomeType & t);
void foo(const SomeType && t);
void foo(volatile SomeType & t);
void foo(volatile SomeType && t);
void foo(const volatile SomeType & t);
void foo(const volatile SomeType && t);

例如:

Widget a;
foo(a);

将实例化:

void foo(Widget & a);

不是:

void foo(Widget a);

因此,所有这些引用都被std::forward删除。只有*modificator* SomeType t被提供给foo()内部的任何函数。