std::forward()
工具函数
定义于头文件 <utility>
。
描述
此工具函数用于保留传递给另一个函数的参数的正确值类别,该函数可以重载其参数的值类别——最常见于包装器风格的委托。
最常见的用例包括
- 不直接调用函数(如上所述重载的函数),而是将其包装到另一个函数中,例如,在传递参数之前记录一些信息。
- 编写一个工厂函数(而不是直接使用构造函数),该函数最终将调用某个构造函数(该构造函数可能会区分右值和左值参数)。
- 编写一个**单个**构造函数,它处理**左值和右值**参数,并将它们转发到成员初始化列表,以构造类的字段(而不是根据其参数的值类别重载该构造函数)。
这种函数为什么必要并不是一目了然。请参阅下面的示例,它们说明了如果省略转发可能发生的问题。
声明
- C++14
- 直到 C++14
- 简化
- 详细
// 1)
template <typename T>
T&& forward(T& t);
// 2)
template <typename T>
T&& forward(T&& t);
// 1)
template <typename T>
constexpr T&& forward( std::remove_reference_t<T>& t ) noexcept;
// 2)
template <typename T>
constexpr T&& forward( std::remove_reference_t<T>&& t ) noexcept;
- 简化
- 详细
// 1)
template <typename T>
T&& forward(T& t);
// 2)
template <typename T>
T&& forward(T&& t);
// 1)
template <typename T>
T&& forward( typename std::remove_reference<T>::type& t ) noexcept;
// 2)
template <typename T>
T&& forward( typename std::remove_reference<T>::type&& t ) noexcept;
参数
t
- 要被*转发*的对象。
返回值
- 简化
- 详细
适当地将 t
强制转换,以正确保留其值类型。
static_cast<T&&>(t)
,由于引用折叠而有效。
复杂度
常数。
示例
*完美转发*是一种有用的功能,有助于保留其参数的原始值类别,这有助于处理样板代码和/或次优实现。
用记录器包装函数
让我们为 consume()
函数创建一个简单的重载,它表示了不同对待左值和右值参数的常见模式。在此示例中,参数未使用,但仍然需要演示稍后使用 std::forward()
的重要性。
#include <iostream>
#include <utility>
#include <string>
void consume(std::string&& message) {
std::cout << "Consumes an rvalue\n";
}
void consume(std::string const& message) {
std::cout << "Consumes an lvalue\n";
}
int main() {
auto msg = std::string("sample message");
consume(msg);
consume("sample message");
}
Consumes an lvalue
Consumes an rvalue
现在,假设我们不想简单地用参数调用 consume()
,而是想将其包装在一个日志函数中,如下所示
// #includes and definitions of consume() omitted for brevity
void log_and_consume(std::string&& message) {
std::cout << "LOG: logging with rvalue\n";
consume(message);
}
void log_and_consume(std::string const& message) {
std::cout << "LOG: logging with lvalue\n";
consume(message);
}
int main() {
auto msg = std::string("sample message");
log_and_consume(msg);
log_and_consume("sample message");
}
人们可能会期望输出是
LOG: logging with lvalue
Consumes an lvalue
LOG: logging with rvalue
Consumes an rvalue
但事实**并非**如此。实际输出是
LOG: logging with lvalue
Consumes an lvalue
LOG: logging with rvalue
Consumes an lvalue
请注意最后一行中的差异。它说的是**左**值,而不是**右**值。
那是因为命名参数总是被视为左值(即使它们的类型是对右值的引用)。要解决这个问题,我们可以使用 std::forward()
void log_and_consume(std::string&& message) {
std::cout << "LOG: logging with rvalue\n";
consume(std::forward<std::string&&>(message));
}
void log_and_consume(std::string const& message) {
std::cout << "LOG: logging with lvalue\n";
consume(std::forward<std::string const&>(message));
}
int main() {
auto msg = std::string("sample message");
log_and_consume(msg);
log_and_consume("sample message");
}
LOG: logging with lvalue
Consumes an lvalue
LOG: logging with rvalue
Consumes an rvalue
std::forward()
可能看起来像一个明确保留正确值类别的类型转换。它实际上就是这样做的。
实现一个能高效处理左值和右值的构造函数
考虑一个包装两个 std::string
的简单类
#include <iostream>
#include <utility>
#include <string>
class person {
std::string name;
std::string surname;
public:
person(std::string const& name, std::string const& surname)
: name(name), surname(surname) { }
};
int main() {
auto name = std::string("Foo");
auto surname = std::string("Bar");
auto p1 = person(name, surname); // 1)
auto p2 = person("Foo", "Bar"); // 2)
}
这很好,但 2)
会受到性能损失。C 字符串字面量("Foo"
和 "Bar"
)将首先需要转换为 std::string
临时对象。临时对象可以绑定到 const&
,因此代码可以编译,但这远非最优。尽管临时对象可以简单地移动到对象中,但它们仍将被用于在 p2
内部创建副本。
为了解决这个问题,person
的构造函数可以重载以接受右值并将其移动到类字段中
person(std::string const& name, std::string const& surname)
: name(name), surname(surname) { }
person(std::string&& name, std::string&& surname)
: name(std::move(name)), surname(std::move(surname)) { }
上一个示例说明了在这种情况下,name
和 surname
是左值(引用绑定到临时对象),因此使用它们初始化 name
和 surname
字段不会调用移动构造函数。这里需要添加 std::move
。
虽然上面的示例有效,但它仍然不是最优的。考虑这种情况
auto p3 = person(name, "Bar");
p3
的创建无法调用接受两个右值(并从它们移动)的构造函数,因为 name
是一个左值。因此,唯一的候选是接受两个 const&
字符串的构造函数。资源被浪费,为 "Bar"
创建临时对象并从中复制。
其中一种解决方案是实现 const&
和 &&
变体的每一个排列
person(std::string const& name, std::string const& surname)
: name(name), surname(surname) { }
person(std::string&& name, std::string&& surname)
: name(std::move(name)), surname(std::move(surname)) { }
person(std::string const& name, std::string&& surname)
: name(name), surname(std::move(surname)) { }
person(std::string&& name, std::string const& surname)
: name(std::move(name)), surname(surname) { }
但这非常麻烦。如果 person
有更多参数,我们将不得不创建更多的重载。
取而代之,我们可以将这些构造函数转换为单个 template
template <typename S1, typename S2>
person(S1&& name, S2&& surname)
: name(/* ??? */ name), surname(/* ??? */ surname) { }
棘手的部分是用什么来代替 /* ??? */
。我们可以在那里什么都不放,但我们永远不会使用移动构造函数(我们同意这样做不是最优的),因为——重复这个重要事实——参数有名称,尽管它们是对右值的引用,但它们的名称被视为左值。我们也不能在那里放 std::move()
,因为在接收左值的情况下,我们不应该从它移动。
解决方案是使用 std::forward()
,如下所示
template <typename S1, typename S2>
person(S1&& name, S2&& surname)
: name(std::forward<S1>(name)), surname(std::forward<S2>(surname)) { }
- 简化
- 详细
如果右值引用 (&&
) 的类型是 template
参数,则会以特殊方式处理它。我们称之为*万能引用*(或*转发引用*†)。
它可以绑定到**左值和右值**。这很方便,因为我们稍后可以使用 std::forward()
来正确地用适当的值类别转发它,而无需真正关心它是右值引用还是左值引用。
†术语*转发引用*源于 std::forward()
的名称。
如果函数模板的函数参数被声明为该函数模板的 cv-unqualified *类型模板参数*的右值引用,我们称之为*万能引用*或*转发引用*。
由于*引用折叠*规则,它可以绑定到左值和右值。
由于我们有两个万能引用参数,我们自动接受任何值类别组合。尽管如此,name
和 surname
是有名称的参数,因此,由于我们可以命名它们,它们被视为左值。为了正确地将它们的原始值类别转发到类字段的构造中,我们使用 std::forward()
。