Wann sollte std :: forward verwendet werden, um Argumente weiterzuleiten?

155

C ++ 0x zeigt ein Beispiel für die Verwendung von std::forward:

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

Wann ist es immer vorteilhaft zu verwenden std::forward?

Außerdem muss es &&in der Parameterdeklaration verwendet werden. Ist es in allen Fällen gültig? Ich dachte, Sie &&müssten temporäre Funktionen an eine Funktion übergeben, wenn die Funktion darin deklariert wurde. Kann foo also mit einem beliebigen Parameter aufgerufen werden?

Zum Schluss, wenn ich einen Funktionsaufruf wie diesen habe:

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

Soll ich das stattdessen verwenden:

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

Wenn Sie die Parameter zweimal in der Funktion verwenden, dh gleichzeitig an zwei Funktionen weiterleiten, ist es sinnvoll, sie zu verwenden std::forward? std::forwardKonvertieren Sie dasselbe nicht zweimal in ein temporäres Element, verschieben Sie den Speicher und machen Sie ihn für eine zweite Verwendung ungültig? Wäre der folgende Code in Ordnung:

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

Ich bin ein bisschen verwirrt von std::forwardund würde gerne etwas aufräumen.

coyotte508
quelle

Antworten:

124

Verwenden Sie es wie Ihr erstes Beispiel:

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

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

Das liegt an den Regeln zum Reduzieren der Referenz : Wenn T = U&, dann T&& = U&, aber wenn T = U&&, dann T&& = U&&haben Sie immer den richtigen Typ im Funktionskörper. Schließlich müssen Sie forwardden lvalue-turn x(weil er jetzt einen Namen hat!) Zurück in eine rvalue-Referenz verwandeln, wenn es ursprünglich eine war.

Sie sollten nicht nach vorne etwas mehr als aber einmal, denn das ist in der Regel nicht sinnvoll: Weiterleitung bedeutet , dass Sie möglicherweise bewegt den ganzen Weg bis zum letzten Anrufer das Argument, und wenn es es weg bewegt hat, so dass Sie nicht dann verwenden können , wieder (so wie du es wahrscheinlich beabsichtigt hast).

Kerrek SB
quelle
Ich dachte es wäre Args...&& args?
Welpe
5
@DeadMG: Es ist immer das Richtige, nicht das, an das ich mich falsch erinnert habe :-) ... obwohl ich es in diesem Fall anscheinend falsch erinnert habe!
Kerrek SB
1
Aber wie wird g für den generischen Typ T deklariert?
MK.
@MK. g wird als reguläre Funktion mit den gewünschten Parametern deklariert.
CoffeDeveloper
1
@cmdLP: Sie haben Recht, dass es gut definiert ist, wiederholt weiterzuleiten, aber es ist selten semantisch korrekt für Ihr Programm. Es ist jedoch ein nützlicher Fall, Mitglieder eines Vorwärtsausdrucks zu nehmen. Ich werde die Antwort aktualisieren.
Kerrek SB
4

Kerreks Antwort ist sehr nützlich, beantwortet aber die Frage aus dem Titel nicht vollständig:

Wann sollte std :: forward verwendet werden, um Argumente weiterzuleiten?

Um dies zu beantworten, sollten wir zunächst einen Begriff universeller Referenzen einführen . Scott Meyers gab diesen Namen und heutzutage werden sie oft als Weiterleitungsreferenzen bezeichnet. Grundsätzlich, wenn Sie so etwas sehen:

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

Denken Sie daran, dass dies paramkeine Wertreferenz ist (wie man zu dem Schluss kommen könnte), sondern eine universelle Referenz *. Universelle Referenzen zeichnen sich durch eine sehr eingeschränkte Form (nur T&&ohne const oder ähnliche Qualifikationsmerkmale) und durch Typabzug aus - der Typ Twird beim fAufrufen abgeleitet. Kurz gesagt, universelle Referenzen entsprechen r-Wert-Referenzen, wenn sie mit r-Werten initialisiert wurden, und l-Wert-Referenzen, wenn sie mit l-Werten initialisiert wurden.

Jetzt ist es relativ einfach, die ursprüngliche Frage zu beantworten - bewerben Sie sich std::forwardbei:

  • Eine universelle Referenz, wenn sie das letzte Mal in der Funktion verwendet wird
  • Eine universelle Referenz wird von Funktionen zurückgegeben, die nach Wert zurückgegeben werden

Ein Beispiel für den ersten Fall:

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
}

Im obigen Code möchten wir propnach Abschluss keinen unbekannten Wert mehr other.set(..)haben, daher erfolgt hier keine Weiterleitung. Wenn barwir jedoch anrufen , leiten wir weiter , sobald propwir damit fertig sind, und barkönnen damit machen, was immer wir wollen (z. B. bewegen).

Ein Beispiel für den zweiten Fall:

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

Diese Funktionsvorlage sollte propin den Rückgabewert verschoben werden, wenn es sich um einen r-Wert handelt, und in den Rückgabewert kopiert werden, wenn es sich um einen l-Wert handelt. Falls wir std::forwardam Ende weggelassen haben , würden wir immer eine Kopie erstellen, was teurer ist, wenn propes sich um einen Wert handelt.

* Um genau zu sein, ist eine universelle Referenz ein Konzept, bei dem eine r-Wert-Referenz auf einen cv-nicht qualifizierten Vorlagenparameter verwendet wird.

Miljen Mikic
quelle
0

Hilft dieses Beispiel? Ich hatte Mühe, ein nützliches, nicht generisches Beispiel für std :: forward zu finden, stieß jedoch auf ein Beispiel für ein Bankkonto, das wir als Argument für die Einzahlung weitergeben.

Wenn wir also eine const-Version eines Kontos haben, sollten wir erwarten, dass die const-Funktion aufgerufen wird, wenn wir sie an unsere Einzahlungsvorlage <> übergeben. und dies löst dann eine Ausnahme aus (die Idee war, dass dies ein gesperrter Account war!)

Wenn wir ein nicht konstantes Konto haben, sollten wir das Konto ändern können.

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

Bauen:

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

Erwartete Ausgabe:

# 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
quelle