When to use std::forward to forward arguments?
C++0x shows an example of using std::forward
:
temp开发者_StackOverflowlate<class T>
void foo(T&& arg)
{
bar(std::forward<T>(arg));
}
When is it advantageous to use std::forward
, always?
Also, it requires to use &&
in the parameters declaration, is it valid in all cases? I thought you had to pass temporaries to a function if the function was declared with &&
in it, so can foo be called with any parameter?
Lastly, if I have a function call such as this:
template<int val, typename... Params>
void doSomething(Params... args) {
doSomethingElse<val, Params...>(args...);
}
Should I use this instead:
template<int val, typename... Params>
void doSomething(Params&&... args) {
doSomethingElse<val, Params...>(std::forward<Params>(args)...);
}
Also, if use the parameters twice in the function, i.e. forwarding to two functions at the same time, is it wise to use std::forward
? Won't std::forward
convert the same thing to a temporary twice, moving the memory and make it invalid for a second use? Would the following code be ok:
template<int val, typename... Params>
void doSomething(Params&&... args) {
doSomethingElse<val, Params...>(std::forward<Params>(args)...);
doSomethingWeird<val, Params...>(std::forward<Params>(args)...);
}
I'm a bit confused by std::forward
, and I'd gladly use some clearing up.
Use it like your first example:
template <typename T> void f(T && x)
{
g(std::forward<T>(x));
}
template <typename ...Args> void f(Args && ...args)
{
g(std::forward<Args>(args)...);
}
That's because of the reference collapsing rules: If T = U&
, then T&& = U&
, but if T = U&&
, then T&& = U&&
, so you always end up with the correct type inside the function body. Finally, you need forward
to turn the lvalue-turned x
(because it has a name now!) back into an rvalue reference if it was one initially.
You should not forward something more than once however, because that usually does not make sense: Forwarding means that you're potentially moving the argument all the way through to the final caller, and once it's moved it's gone, so you cannot then use it again (in the way you probably meant to).
Kerrek's answer is very useful, but it doesn't completely answer the question from the title:
When to use std::forward to forward arguments?
In order to answer it, we should first introduce a notion of universal references. Scott Meyers gave this name and nowadays they are often called forwarding references. Basically, when you see something like this:
template<typename T>
void f(T&& param);
bear in mind that param
is not an rvalue reference (as one may be tempted to conclude), but a universal reference*. Universal references are characterized by a very restricted form (just T&&
, without const or similar qualifiers) and by type deduction - the type T
will be deduced when f
is invoked. In a nutshell, universal references correspond to rvalue references if they’re initialized with
rvalues, and to lvalue references if they’re initialized with lvalues.
Now it's relatively easy to answer the original question - apply std::forward
to:
- a universal reference the last time it’s used in the function
- a universal reference being returned from functions that return by value
An example for the first case:
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
}
In the code above, we don't want prop
to have some unknown value after other.set(..)
has finished, so no forwarding happens here. However, when calling bar
we forward prop
as we are done with it and bar
can do whatever it wants with it (e.g. move it).
An example for the second case:
template<typename T>
Widget transform(T&& prop) {
prop.transform();
return std::forward<T>(prop);
}
This function template should move prop
into the return value if it's an rvalue and copy it if it's an lvalue. In case that we omitted std::forward
at the end, we would always create a copy, which is more expensive when prop
happens to be an rvalue.
*to be fully precise, a universal reference is a concept of taking an rvalue reference to a cv-unqualified template parameter.
Does this example help? I struggled to find a useful non generic example of std::forward, but hit upon an example of a bank account that we pass along the cash to be deposited as an argument.
So if we have a const version of an account we should expect when we pass it to our deposit template<> that the const function is called; and this then throws an exception (the idea being this was a locked account!)
If we have a non const account then we should be able to modify the account.
#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;
}
}
To build:
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
Expected output:
# 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
精彩评论