Kapan harus menggunakan argumen std :: forward to forward?

155

C ++ 0x menunjukkan contoh menggunakan std::forward:

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

Kapan menguntungkan untuk digunakan std::forward, selalu?

Selain itu, harus digunakan &&dalam deklarasi parameter, apakah valid dalam semua kasus? Saya pikir Anda harus memberikan temporari ke suatu fungsi jika fungsi tersebut dideklarasikan &&dengannya, jadi bisakah foo dipanggil dengan parameter apa saja?

Terakhir, jika saya memiliki panggilan fungsi seperti ini:

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

Haruskah saya menggunakan ini sebagai gantinya:

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

Juga, jika menggunakan parameter dua kali dalam fungsi, yaitu meneruskan ke dua fungsi pada saat yang sama, apakah bijaksana untuk menggunakannya std::forward? Tidak akan std::forwardmengubah hal yang sama menjadi sementara dua kali, memindahkan memori dan membuatnya tidak valid untuk penggunaan kedua? Apakah kode berikut tidak masalah:

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

Saya agak bingung dengan std::forward, dan saya dengan senang hati menggunakan beberapa kliring.

coyotte508
sumber

Jawaban:

124

Gunakan seperti contoh pertama Anda:

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

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

Itu karena aturan runtuh referensi : Jika T = U&, lalu T&& = U&, tetapi jika T = U&&, maka T&& = U&&, maka Anda selalu berakhir dengan jenis yang benar di dalam fungsi tubuh. Akhirnya, Anda perlu forwardmengubah nilai-berubah x(karena sekarang memiliki nama!) Kembali menjadi referensi nilai jika itu awalnya.

Namun, Anda tidak boleh meneruskan sesuatu lebih dari sekali, karena itu biasanya tidak masuk akal: Penerusan berarti Anda berpotensi memindahkan argumen sampai ke penelepon terakhir, dan setelah dipindahkan, sudah tidak ada, jadi Anda tidak dapat menggunakannya lagi (dengan cara yang mungkin Anda maksudkan).

Kerrek SB
sumber
Saya pikir itu Args...&& args?
Puppy
5
@DeadMG: Selalu yang benar, bukan yang saya salah ingat :-) ... meskipun dalam hal ini saya sepertinya salah mengartikannya dengan benar!
Kerrek SB
1
Tetapi bagaimana g dinyatakan untuk tipe T generik?
MK.
@MK. g dideklarasikan sebagai fungsi reguler dengan parameter yang Anda inginkan.
CoffeDeveloper
1
@ cmdLP: Anda benar bahwa itu didefinisikan dengan baik untuk meneruskan berulang kali, tetapi jarang secara semantik benar untuk program Anda. Mengambil anggota ekspresi maju adalah kasus yang bermanfaat. Saya akan memperbarui jawabannya.
Kerrek SB
4

Jawaban Kerrek sangat berguna, tetapi tidak sepenuhnya menjawab pertanyaan dari judul:

Kapan harus menggunakan argumen std :: forward to forward?

Untuk menjawabnya, pertama-tama kita harus memperkenalkan gagasan tentang referensi universal . Scott Meyers memberikan nama ini dan saat ini mereka sering disebut referensi penerusan. Pada dasarnya, ketika Anda melihat sesuatu seperti ini:

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

ingatlah bahwa paramitu bukan referensi nilai (karena orang mungkin tergoda untuk menyimpulkan), tetapi referensi universal *. Referensi universal dicirikan oleh bentuk yang sangat terbatas (hanya T&&, tanpa const atau kualifikasi yang serupa) dan dengan tipe deduksi - tipe Takan dideduksi ketika fdipanggil. Singkatnya, referensi universal sesuai dengan referensi nilai jika mereka diinisialisasi dengan nilai-nilai, dan untuk menilai nilai referensi jika mereka diinisialisasi dengan nilai-nilai.

Sekarang relatif mudah untuk menjawab pertanyaan awal - berlaku std::forwarduntuk:

  • referensi universal yang terakhir kali digunakan dalam fungsi
  • referensi universal dikembalikan dari fungsi yang mengembalikan nilai

Contoh untuk kasus pertama:

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
}

Dalam kode di atas, kami tidak ingin propmemiliki nilai yang tidak diketahui setelah other.set(..)selesai, jadi tidak ada penerusan yang terjadi di sini. Namun, ketika memanggil barkita maju propkarena kita sudah selesai dengan itu dan bardapat melakukan apa pun yang diinginkan dengan itu (misalnya memindahkannya).

Contoh untuk kasus kedua:

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

Templat fungsi ini harus pindah propke nilai kembali jika itu adalah nilai dan salin jika itu adalah nilai. Jika kita dihilangkan std::forwardpada akhirnya, kita akan selalu membuat salinan, yang lebih mahal ketika propterjadi suatu nilai.

* agar sepenuhnya tepat, referensi universal adalah konsep mengambil referensi nilai ke parameter templat cv-wajar tanpa pengecualian.

Miljen Mikic
sumber
0

Apakah contoh ini membantu? Saya berjuang untuk menemukan contoh non generik yang berguna dari std :: forward, tetapi menemukan contoh rekening bank yang kami berikan uang tunai untuk disimpan sebagai argumen.

Jadi jika kita memiliki versi const dari sebuah akun, kita harus berharap ketika kita meneruskannya ke template setoran kita <> sehingga fungsi const dipanggil; dan ini kemudian melemparkan pengecualian (gagasan bahwa ini adalah akun yang terkunci!)

Jika kita memiliki akun non-const maka kita harus dapat mengubah akun.

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

Untuk membangun:

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

Output yang diharapkan:

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