跳到主要内容

std::forward() 工具函数

定义于头文件 <utility>

描述

此工具函数用于保留传递给另一个函数的参数的正确值类别,该函数可以重载其参数的值类别——最常见于包装器风格的委托。

最常见的用例包括

  • 不直接调用函数(如上所述重载的函数),而是将其包装到另一个函数中,例如,在传递参数之前记录一些信息。
  • 编写一个工厂函数(而不是直接使用构造函数),该函数最终将调用某个构造函数(该构造函数可能会区分右值和左值参数)。
  • 编写一个**单个**构造函数,它处理**左值和右值**参数,并将它们转发到成员初始化列表,以构造类的字段(而不是根据其参数的值类别重载该构造函数)。

这种函数为什么必要并不是一目了然。请参阅下面的示例,它们说明了如果省略转发可能发生的问题。

声明

// 1)
template <typename T>
T&& forward(T& t);

// 2)
template <typename T>
T&& forward(T&& t);

参数

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)) { }

上一个示例说明了在这种情况下,namesurname 是左值(引用绑定到临时对象),因此使用它们初始化 namesurname 字段不会调用移动构造函数。这里需要添加 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() 的名称。

由于我们有两个万能引用参数,我们自动接受任何值类别组合。尽管如此,namesurname 是有名称的参数,因此,由于我们可以命名它们,它们被视为左值。为了正确地将它们的原始值类别转发到类字段的构造中,我们使用 std::forward()

std::forward() 工具函数

定义于头文件 <utility>

描述

此工具函数用于保留传递给另一个函数的参数的正确值类别,该函数可以重载其参数的值类别——最常见于包装器风格的委托。

最常见的用例包括

  • 不直接调用函数(如上所述重载的函数),而是将其包装到另一个函数中,例如,在传递参数之前记录一些信息。
  • 编写一个工厂函数(而不是直接使用构造函数),该函数最终将调用某个构造函数(该构造函数可能会区分右值和左值参数)。
  • 编写一个**单个**构造函数,它处理**左值和右值**参数,并将它们转发到成员初始化列表,以构造类的字段(而不是根据其参数的值类别重载该构造函数)。

这种函数为什么必要并不是一目了然。请参阅下面的示例,它们说明了如果省略转发可能发生的问题。

声明

// 1)
template <typename T>
T&& forward(T& t);

// 2)
template <typename T>
T&& forward(T&& t);

参数

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)) { }

上一个示例说明了在这种情况下,namesurname 是左值(引用绑定到临时对象),因此使用它们初始化 namesurname 字段不会调用移动构造函数。这里需要添加 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() 的名称。

由于我们有两个万能引用参数,我们自动接受任何值类别组合。尽管如此,namesurname 是有名称的参数,因此,由于我们可以命名它们,它们被视为左值。为了正确地将它们的原始值类别转发到类字段的构造中,我们使用 std::forward()