Quand utiliser std :: forward pour transférer des arguments?

155

C ++ 0x montre un exemple d'utilisation std::forward:

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

Quand est-il toujours avantageux de l'utiliser std::forward?

En outre, il nécessite de l'utiliser &&dans la déclaration des paramètres, est-ce valable dans tous les cas? Je pensais que vous deviez passer des temporaires à une fonction si la fonction était déclarée avec &&, alors peut-on appeler foo avec n'importe quel paramètre?

Enfin, si j'ai un appel de fonction comme celui-ci:

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

Dois-je utiliser ceci à la place:

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

De plus, si vous utilisez les paramètres deux fois dans la fonction, c'est-à-dire le renvoi vers deux fonctions en même temps, est-il judicieux d'utiliser std::forward? La std::forwardmême chose ne sera-t-elle pas convertie deux fois en temporaire, en déplaçant la mémoire et en la rendant invalide pour une deuxième utilisation? Le code suivant serait-il correct:

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

Je suis un peu confus std::forward, et j'utiliserais volontiers quelques éclaircissements.

coyotte508
la source

Réponses:

124

Utilisez-le comme votre premier exemple:

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

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

C'est à cause des règles de réduction des références : si T = U&, alors T&& = U&, mais si T = U&&, alors T&& = U&&, vous vous retrouvez toujours avec le type correct dans le corps de la fonction. Enfin, vous devez forwardreconvertir la lvalue x(car elle a un nom maintenant!) En une référence rvalue s'il en était une au départ.

Cependant, vous ne devriez pas transférer quelque chose plus d'une fois, car cela n'a généralement pas de sens: le transfert signifie que vous transférez potentiellement l'argument jusqu'à l'appelant final, et une fois qu'il a été déplacé, il est parti, vous ne pouvez donc pas l'utiliser. encore une fois (de la manière que vous vouliez probablement).

Kerrek SB
la source
Je pensais que c'était Args...&& args?
Chiot
5
@DeadMG: C'est toujours celui qui est correct, pas celui dont je me souviens mal :-) ... bien que dans ce cas, il me semble que je m'en suis bien souvenu!
Kerrek SB
1
Mais comment g déclaré pour le type générique T?
MK.
@MK. g est déclaré comme une fonction régulière avec les paramètres souhaités.
CoffeDeveloper
1
@cmdLP: Vous avez raison de dire qu'il est bien défini pour transmettre à plusieurs reprises, mais il est rarement sémantiquement correct pour votre programme. Faire avancer les membres d'une expression en avant est cependant un cas utile. Je mettrai à jour la réponse.
Kerrek SB
4

La réponse de Kerrek est très utile, mais elle ne répond pas complètement à la question du titre:

Quand utiliser std :: forward pour transférer des arguments?

Pour y répondre, il faut d'abord introduire une notion de références universelles . Scott Meyers a donné ce nom et de nos jours on les appelle souvent des références de transmission. En gros, quand vous voyez quelque chose comme ça:

template<typename T>
void f(T&& param);

gardez à l'esprit qu'il paramne s'agit pas d'une référence rvalue (comme on peut être tenté de le conclure), mais d'une référence universelle *. Les références universelles sont caractérisées par une forme très restreinte (juste T&&, sans const ou qualificatifs similaires) et par une déduction de type - le type Tsera déduit lors de l' fappel. En un mot, les références universelles correspondent aux références rvalue si elles sont initialisées avec rvalues, et aux références lvalue si elles sont initialisées avec lvalues.

Maintenant, il est relativement facile de répondre à la question initiale - appliquer std::forwardà:

  • une référence universelle la dernière fois qu'elle est utilisée dans la fonction
  • une référence universelle renvoyée par des fonctions qui retournent par valeur

Un exemple pour le premier cas:

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
}

Dans le code ci-dessus, nous ne voulons proppas avoir de valeur inconnue après la other.set(..)fin, donc aucun transfert ne se produit ici. Cependant, lors de l'appel, barnous avançons propcomme nous en avons terminé avec lui et barpouvons faire ce qu'il veut (par exemple, le déplacer).

Un exemple pour le deuxième cas:

template<typename T>
Widget transform(T&& prop) {
   prop.transform();
   return std::forward<T>(prop);
}

Ce modèle de fonction doit se déplacer propdans la valeur de retour s'il s'agit d'une rvalue et la copier s'il s'agit d'une lvalue. Dans le cas où nous aurions omis std::forwardà la fin, nous propcréerions toujours une copie, ce qui est plus cher quand il s'agit d'une rvalue.

* pour être tout à fait précis, une référence universelle est un concept de prise d'une référence rvalue à un paramètre de modèle non qualifié cv.

Miljen Mikic
la source
0

Cet exemple vous aide-t-il? J'ai eu du mal à trouver un exemple non générique utile de std :: forward, mais je suis tombé sur un exemple de compte bancaire que nous transmettons l'argent à déposer en argument.

Donc, si nous avons une version const d'un compte, nous devrions nous attendre lorsque nous le passons à notre modèle de dépôt <> que la fonction const est appelée; et cela lève alors une exception (l'idée étant qu'il s'agissait d'un compte verrouillé!)

Si nous avons un compte non const, nous devrions pouvoir modifier le compte.

#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;
    }
}

Construire:

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

Production attendue:

# 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
Neil McGill
la source