何时使用std::forward转发参数?

174

C++0x 展示了使用 std::forward 的一个例子:

template<class T>
void foo(T&& arg) 
{
  bar(std::forward<T>(arg));
}

什么时候使用 std::forward 是有优势的,总是吗?

另外,它需要在参数声明中使用 &&,这在所有情况下都有效吗?我认为如果函数在声明中带有 &&,则必须将临时变量传递给函数,那么 foo 可以用任何参数调用吗?

最后,如果我有以下函数调用:

template<int val, typename... Params>
void doSomething(Params... args) {
  doSomethingElse<val, Params...>(args...);
}

我应该使用这个替代品吗:

template<int val, typename... Params>
void doSomething(Params&&... args) {
  doSomethingElse<val, Params...>(std::forward<Params>(args)...);
}

另外,如果在函数中两次使用同一参数,即同时转发到两个函数中,使用std::forward是否明智?std::forward不会将相同的东西转换为临时对象两次吗?这样会移动内存并使其对第二次使用无效吗?以下代码是否可以:

template<int val, typename... Params>
void doSomething(Params&&... args) {
  doSomethingElse<val, Params...>(std::forward<Params>(args)...);
  doSomethingWeird<val, Params...>(std::forward<Params>(args)...);
}

我对 std::forward 有点困惑,希望能得到一些解释。


3个回答

145

请像第一个例子一样使用它:

template <typename T> void f(T && x)
{
  g(std::forward<T>(x));
}

template <typename ...Args> void f(Args && ...args)
{
  g(std::forward<Args>(args)...);
}

那是因为引用折叠规则的缘故:如果T = U&,那么T&& = U&,但如果T = U&&,那么T&& = U&&,所以你总是能在函数体内得到正确的类型。最后,你需要forward将命名的左值引用x(因为它现在有名字了!)转换回右值引用(如果它最初是一个右值引用)。
不过,你不应该多次转发同一物件,因为这通常没有意义:转发意味着你可能会把参数一直传递到最终的调用者,一旦它被移动了,它就消失了,所以你不能再次使用它(以你可能想象的方式)。

我以为是 Args...&& args - Puppy
6
@DeadMG:通常是正确的那个,而不是我记错了的那个 :-) ...尽管在这种情况下,我似乎已经正确地记住了错误的信息! - Kerrek SB
1
但是如何为泛型类型T声明g? - MK.
@MK。g被声明为一个带有所需参数的常规函数。 - CoffeDeveloper
1
在函数中多次转发变量是完全合法的。例如,如果您想要转发结构体的成员,可以在结构体上两次使用“std::forward”,然后访问转发的结构体的成员时,将结构体的一部分移动到新位置,结构体的成员被清空,就像对于“std::string”一样。转发或移动永远不会使结构体无效。而且,只写“std::move(my_string);”或“std::forward<T>(val);”作为单个语句是无操作的。 - cmdLP
2
@cmdLP:你说得对,重复转发是有明确定义的,但在程序中很少语义上正确。不过,取前向表达式的成员是一个有用的情况。我会更新答案。 - Kerrek SB

12
Kerrek的回答很有用,但并没有完全回答标题中的问题:
何时使用std::forward来转发参数?
为了回答这个问题,我们首先需要介绍通用引用的概念。Scott Meyers提出了这个名字,现在它们通常被称为转发引用。基本上,当你看到像这样的东西时:
template<typename T>
void f(T&& param);

请记住,{{param}}不是一个右值引用(尽管人们可能会得出这样的结论),而是一种通用引用*。通用引用的特点是非常受限制的形式(只有T&&,没有const或类似的限定符)和类型推导 - 当调用f时,类型T将被推导出来。简而言之,如果使用rvalue初始化,通用引用对应于rvalue引用,如果使用lvalue初始化,它们对应于lvalue引用。
现在很容易回答最初的问题 - 对以下内容应用std::forward
  • 在函数中最后一次使用通用引用
  • 从返回值为值的函数返回的通用引用
第一个情况的示例:
template<typename T>
void foo(T&& prop) {
    other.set(prop); // use prop, but don't modify it because we still need it
    bar(std::forward<T>(prop)); // final use -> std::forward
}

在上面的代码中,我们不希望在other.set(..)完成后prop具有某些未知值,因此这里没有进行转发。然而,在调用bar时,我们将prop转发,因为我们已经使用完毕,bar可以对其进行任何想要的操作(例如移动)。
第二种情况的示例:
template<typename T>
Widget transform(T&& prop) {
   prop.transform();
   return std::forward<T>(prop);
}

如果prop是右值,此函数模板应将其移动到返回值中,如果是左值,则复制它。如果我们在结尾处省略了std::forward,则总是会创建一个副本,当prop恰好是右值时,这更加昂贵。

*要完全准确,通用引用是指对cv未限定的模板参数进行右值引用的概念。


2
这个例子有帮助吗?我曾经苦于找不到一个有用的非通用std::forward示例,但是想到了一个银行账户的例子,我们将要存入的现金作为参数传递。
因此,如果我们有一个帐户的const版本,当我们将其传递给我们的deposit模板<>时,应该调用const函数;然后抛出异常(想法是这是一个锁定的帐户!)
如果我们有一个非const帐户,那么我们应该能够修改帐户。
#include <iostream>
#include <string>
#include <sstream> // std::stringstream
#include <algorithm> // std::move
#include <utility>
#include <iostream>
#include <functional>

template<class T> class BankAccount {
private:
    const T no_cash {};
    T cash {};
public:
    BankAccount<T> () {
        std::cout << "default constructor " << to_string() << std::endl;
    }
    BankAccount<T> (T cash) : cash (cash) {
        std::cout << "new cash " << to_string() << std::endl;
    }
    BankAccount<T> (const BankAccount& o) {
        std::cout << "copy cash constructor called for " << o.to_string() << std::endl;
        cash = o.cash;
        std::cout << "copy cash constructor result is  " << to_string() << std::endl;
    }
    // Transfer of funds?
    BankAccount<T> (BankAccount<T>&& o) {
        std::cout << "move cash called for " << o.to_string() << std::endl;
        cash = o.cash;
        o.cash = no_cash;
        std::cout << "move cash result is  " << to_string() << std::endl;
    }
    ~BankAccount<T> () {
        std::cout << "delete account " << to_string() << std::endl;
    }
    void deposit (const T& deposit) {
        cash += deposit;
        std::cout << "deposit cash called " << to_string() << std::endl;
    }
    friend int deposit (int cash, const BankAccount<int> &&account) {
        throw std::string("tried to write to a locked (const) account");
    }
    friend int deposit (int cash, const BankAccount<int> &account) {
        throw std::string("tried to write to a locked (const) account");
    }
    friend int deposit (int cash, BankAccount<int> &account) {
        account.deposit(cash);
        return account.cash;
    }
    friend std::ostream& operator<<(std::ostream &os, const BankAccount<T>& o) {
        os << "$" << std::to_string(o.cash);
        return os;
    }
    std::string to_string (void) const {
        auto address = static_cast<const void*>(this);
        std::stringstream ss;
        ss << address;
        return "BankAccount(" + ss.str() + ", cash $" + std::to_string(cash) + ")";
    }
};

template<typename T, typename Account>
int process_deposit(T cash, Account&& b) {
    return deposit(cash, std::forward<Account>(b));
}

int main(int, char**)
{
    try {
        // create account1 and try to deposit into it
        auto account1 = BankAccount<int>(0);
        process_deposit<int>(100, account1);
        std::cout << account1.to_string() << std::endl;
        std::cout << "SUCCESS: account1 deposit succeeded!" << std::endl;
    } catch (const std::string &e) {
        std::cerr << "FAILED: account1 deposit failed!: " << e << std::endl;
    }

    try {
        // create locked account2 and try to deposit into it; this should fail
        const auto account2 = BankAccount<int>(0);
        process_deposit<int>(100, account2);
        std::cout << account2.to_string() << std::endl;
        std::cout << "SUCCESS: account2 deposit succeeded!" << std::endl;
    } catch (const std::string &e) {
        std::cerr << "FAILED: account2 deposit failed!: " << e << std::endl;
    }

    try {
        // create locked account3 and try to deposit into it; this should fail
        auto account3 = BankAccount<int>(0);
        process_deposit<int>(100, std::move(account3));
        std::cout << account3.to_string() << std::endl;
        std::cout << "SUCCESS: account3 deposit succeeded!" << std::endl;
    } catch (const std::string &e) {
        std::cerr << "FAILED: account3 deposit failed!: " << e << std::endl;
    }
}

构建:

要构建:

cd std_forward
rm -f *.o example
c++ -std=c++2a -Werror -g -ggdb3 -Wall -c -o main.o main.cpp
c++ main.o  -o example
./example

预期输出:
# create account1 and try to deposit into it
new cash BankAccount(0x7ffee68d96b0, cash $0)
deposit cash called BankAccount(0x7ffee68d96b0, cash $100)
BankAccount(0x7ffee68d96b0, cash $100)
# SUCCESS: account1 deposit succeeded!
delete account BankAccount(0x7ffee68d96b0, cash $100)

# create locked account2 and try to deposit into it; this should fail
new cash BankAccount(0x7ffee68d9670, cash $0)
delete account BankAccount(0x7ffee68d9670, cash $0)
# FAILED: account2 deposit failed!: tried to write to a locked (const) account

# create locked account3 and try to deposit into it; this should fail
new cash BankAccount(0x7ffee68d9630, cash $0)
delete account BankAccount(0x7ffee68d9630, cash $0)
# FAILED: account3 deposit failed!: tried to write to a locked (const) account

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接