Kiedy używać std :: forward do przekazywania argumentów?

155

C ++ 0x pokazuje przykład użycia std::forward:

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

Kiedy std::forwardzawsze warto używać ?

Ponadto wymaga użycia &&w deklaracji parametrów, czy jest ważny we wszystkich przypadkach? Myślałem, że musisz przekazać tymczasowe do funkcji, jeśli funkcja została &&w niej zadeklarowana , więc czy można wywołać foo z dowolnym parametrem?

Na koniec, jeśli mam wywołanie funkcji, takie jak to:

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

Powinienem zamiast tego użyć:

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

Ponadto, jeśli dwukrotnie użyjesz parametrów w funkcji, tj. Przekażesz do dwóch funkcji w tym samym czasie, czy rozsądnie jest to zrobić std::forward? Czy nie std::forwardprzekonwertujesz tego samego na tymczasowy dwukrotnie, przenosząc pamięć i uniemożliwiając jej ponowne użycie? Czy poniższy kod byłby w porządku:

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

Jestem trochę zdezorientowany std::forwardi chętnie użyłbym jakiegoś wyjaśnienia.

coyotte508
źródło

Odpowiedzi:

124

Użyj go jak pierwszego przykładu:

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

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

Dzieje się tak z powodu reguł zwijania referencji : jeśli T = U&, to T&& = U&, ale jeśli T = U&&, to T&& = U&&, więc zawsze otrzymujesz poprawny typ w treści funkcji. Na koniec musisz forwardzmienić l-wartość-obróconą x(ponieważ ma teraz nazwę!) Z powrotem w odniesienie do r-wartości, jeśli była nim początkowo.

Nie powinieneś jednak przekazywać czegoś więcej niż raz, ponieważ zwykle nie ma to sensu: Przekazywanie oznacza, że ​​potencjalnie przenosisz argument przez całą drogę do ostatniego dzwoniącego, a po przeniesieniu go nie ma, więc nie możesz go później użyć ponownie (w sposób, w jaki prawdopodobnie zamierzałeś).

Kerrek SB
źródło
Myślałem, że tak Args...&& args?
Puppy
5
@DeadMG: To zawsze jest poprawna, a nie ta, którą źle zapamiętałem :-) ... chociaż w tym przypadku chyba źle ją zapamiętałem!
Kerrek SB
1
Ale w jaki sposób deklaruje się g dla ogólnego typu T?
MK.
@MK. g jest zadeklarowana jako zwykła funkcja z wymaganymi parametrami.
CoffeDeveloper
1
@cmdLP: Masz rację, że jest dobrze zdefiniowane, aby wielokrotnie przekazywać dalej, ale rzadko jest semantycznie poprawne dla twojego programu. Przyjmowanie członków wyrażeń do przodu jest jednak użytecznym przypadkiem. Zaktualizuję odpowiedź.
Kerrek SB
4

Odpowiedź Kerreka jest bardzo przydatna, ale nie do końca odpowiada na pytanie z tytułu:

Kiedy używać std :: forward do przekazywania argumentów?

Aby na nie odpowiedzieć, należałoby najpierw wprowadzić pojęcie uniwersalnych odniesień . Scott Meyers nadał tę nazwę i obecnie są one często nazywane referencjami spedycyjnymi. Zasadniczo, gdy widzisz coś takiego:

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

pamiętaj, że paramnie jest to odniesienie do wartości r (jak można by pokusić się z wnioskiem), ale odniesienie uniwersalne *. Odwołania uniwersalne charakteryzują się bardzo ograniczoną formą (tylko T&&, bez stałych lub podobnych kwalifikatorów) i dedukcją typu - typ Tzostanie wydedukowany po fwywołaniu. W skrócie, odwołania uniwersalne odpowiadają referencjom r-wartości, jeśli są zainicjowane rvalues, i referencjom l-wartości, jeśli są zainicjowane lvalues.

Teraz stosunkowo łatwo odpowiedzieć na pierwotne pytanie - aplikuj std::forwarddo:

  • uniwersalne odwołanie podczas ostatniego użycia w funkcji
  • uniwersalne odwołanie zwracane przez funkcje, które zwracają wartość

Przykład dla pierwszego przypadku:

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
}

W powyższym kodzie nie chcemy propmieć nieznanej wartości po other.set(..)zakończeniu, więc nie ma tu żadnego przekazywania. Jednak dzwoniąc bar, przekazujemy dalej, propponieważ już z nim skończyliśmy i barmożemy z nim zrobić, co chce (np. Przenieść).

Przykład dla drugiego przypadku:

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

Ten szablon funkcji powinien zostać przeniesiony propdo wartości zwracanej, jeśli jest to wartość r, i skopiować, jeśli jest to wartość l. W przypadku gdybyśmy pominęli std::forwardna końcu, zawsze stworzylibyśmy kopię, która jest droższa, gdy propjest rvalue.

* aby być w pełni precyzyjnym, uniwersalnym odniesieniem jest koncepcja wzięcia odniesienia r-wartości do niekwalifikowanego parametru szablonu cv.

Miljen Mikic
źródło
0

Czy ten przykład pomaga? Usiłowałem znaleźć użyteczny, nieogólny przykład std :: forward, ale trafiłem na przykład konta bankowego, które przekazujemy jako argument do zdeponowania gotówki.

Więc jeśli mamy stałą wersję konta, powinniśmy się spodziewać, że po przekazaniu jej do naszego szablonu depozytu <> zostanie wywołana funkcja const; a to następnie zgłasza wyjątek (chodziło o to, że było to zablokowane konto!)

Jeśli mamy konto inne niż const, powinniśmy być w stanie je zmodyfikować.

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

Budować:

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

Oczekiwany wynik:

# 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
źródło