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.

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ś).


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.


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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.