Apa tujuan utama penggunaan std :: forward dan masalah apa yang dipecahkannya?

438

Dalam penerusan yang sempurna, std::forwarddigunakan untuk mengkonversi referensi nilai yang dinamai t1dan referensi nilai yang t2tidak disebutkan namanya. Apa tujuan melakukan itu? Bagaimana hal itu memengaruhi fungsi yang dipanggil innerjika kita meninggalkan t1& t2sebagai lvalues?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
Steveng
sumber

Jawaban:

789

Anda harus memahami masalah penerusan. Anda dapat membaca seluruh masalah secara detail , tetapi saya akan meringkasnya.

Pada dasarnya, mengingat ungkapan itu E(a, b, ... , c), kami ingin f(a, b, ... , c)persamaan itu setara. Di C ++ 03, ini tidak mungkin. Ada banyak upaya, tetapi semuanya gagal dalam beberapa hal.


Yang paling sederhana adalah menggunakan referensi-nilai:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
    E(a, b, c);
}

Tetapi ini gagal menangani nilai sementara:, f(1, 2, 3);karena itu tidak dapat terikat pada referensi-nilai.

Upaya selanjutnya mungkin:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(a, b, c);
}

Yang memperbaiki masalah di atas, tetapi membalik jepit. Sekarang gagal untuk memungkinkan Euntuk memiliki argumen non-const:

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these

Usaha ketiga menerima-referensi const, tapi kemudian const_cast's constaway:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

Ini menerima semua nilai, bisa meneruskan semua nilai, tetapi berpotensi mengarah pada perilaku yang tidak terdefinisi:

const int i = 1, j = 2, k = 3;
E(int&, int&, int&); f(i, j, k); // ouch! E can modify a const object!

Solusi akhir menangani semuanya dengan benar ... dengan biaya yang mustahil untuk dipertahankan. Anda memberikan kelebihan f, dengan semua kombinasi const dan non-const:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

N argumen membutuhkan 2 kombinasi N , mimpi buruk. Kami ingin melakukan ini secara otomatis.

(Ini secara efektif apa yang kita dapatkan dari kompiler untuk kita di C ++ 11.)


Di C ++ 11, kami mendapat kesempatan untuk memperbaikinya. Satu solusi memodifikasi aturan pengurangan template pada tipe yang ada, tetapi ini berpotensi memecah banyak kode. Jadi kita harus mencari cara lain.

Solusinya adalah dengan menggunakan referensi- nilai yang baru ditambahkan ; kita dapat memperkenalkan aturan baru ketika menyimpulkan tipe referensi nilai dan membuat hasil yang diinginkan. Lagipula, kita tidak mungkin memecahkan kode sekarang.

Jika diberi referensi ke referensi (catatan referensi adalah istilah yang mencakup arti keduanya T&dan T&&), kami menggunakan aturan berikut untuk mencari tahu tipe yang dihasilkan:

"[diberikan] tipe TR yang merupakan referensi ke tipe T, upaya untuk membuat tipe" referensi nilai ke cv TR "menciptakan tipe" referensi nilai ke T ", sementara upaya untuk membuat tipe" referensi nilai ke cv TR ”menciptakan tipe TR."

Atau dalam bentuk tabel:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

Selanjutnya, dengan pengurangan argumen templat: jika argumen adalah nilai l A, kami menyediakan argumen templat dengan referensi nilai lv untuk A. Jika tidak, kami menyimpulkan secara normal. Ini memberikan apa yang disebut referensi universal (istilah penerusan referensi sekarang menjadi yang resmi).

Mengapa ini berguna? Karena gabungan, kami mempertahankan kemampuan untuk melacak kategori nilai dari suatu jenis: jika itu adalah nilai, kami memiliki parameter referensi-nilai, jika tidak, kami memiliki parameter referensi-nilai.

Dalam kode:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

Hal terakhir adalah "meneruskan" kategori nilai variabel. Perlu diingat, begitu berada di dalam fungsi, parameter dapat diteruskan sebagai nilai untuk apa pun:

void foo(int&);

template <typename T>
void deduce(T&& x)
{
    foo(x); // fine, foo can refer to x
}

deduce(1); // okay, foo operates on x which has a value of 1

Itu tidak baik. E perlu mendapatkan kategori nilai yang sama dengan yang kami dapatkan! Solusinya adalah ini:

static_cast<T&&>(x);

Apa fungsinya? Anggap kita berada di dalam deducefungsi, dan kita telah melewati suatu nilai. Ini berarti Ta A&, dan tipe target untuk pemeran statis adalah A& &&, atau adil A&. Karena xsudah menjadi A&, kami tidak melakukan apa-apa dan dibiarkan dengan referensi nilai tinggi.

Ketika kita telah melewati nilai, Tadalah A, jadi tipe target untuk pemeran statis adalah A&&. Para pemain menghasilkan ekspresi nilai, yang tidak lagi dapat diteruskan ke referensi nilai . Kami telah mempertahankan kategori nilai parameter.

Menyatukan ini memberi kita "penerusan sempurna":

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

Ketika fmenerima nilai, Emendapat nilai. Saat fmenerima nilai, Edapatkan nilai. Sempurna.


Dan tentu saja, kami ingin menyingkirkan yang jelek. static_cast<T&&>samar dan aneh untuk diingat; mari kita buat fungsi utilitas yang disebut forward, yang melakukan hal yang sama:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
GManNickG
sumber
1
Bukankah ffungsi, dan bukan ekspresi?
Michael Foukarakis
1
Upaya terakhir Anda tidak benar terkait dengan pernyataan masalah: Ini akan meneruskan nilai const sebagai non-const, sehingga tidak meneruskan sama sekali. Perhatikan juga bahwa dalam upaya pertama, const int iakan diterima: Adisimpulkan const int. Kegagalan adalah untuk nilai-nilai literal. Perhatikan juga bahwa untuk panggilan ke deduced(1), x adalah int&&, tidak int(penerusan yang sempurna tidak pernah membuat salinan, seperti yang akan dilakukan jika xakan menjadi parameter dengan nilai). Hanya Titu int. Alasan yang xmengevaluasi nilai lv dalam forwarder adalah karena rujukan yang dinamai rvalue menjadi ekspresi lvalue.
Johannes Schaub - litb
5
Apakah ada perbedaan dalam menggunakan forwardatau di movesini? Atau hanya perbedaan semantik?
0x499602D2
28
@ David: std::moveharus dipanggil tanpa argumen templat eksplisit dan selalu menghasilkan nilai, sementara std::forwardmungkin berakhir juga. Gunakan std::movesaat Anda tahu Anda tidak lagi membutuhkan nilai dan ingin memindahkannya ke tempat lain, gunakan std::forwarduntuk melakukannya sesuai dengan nilai yang diteruskan ke templat fungsi Anda.
GManNickG
5
Terima kasih telah memulai dengan contoh nyata terlebih dahulu dan memotivasi masalah; sangat membantu!
ShreevatsaR
61

Saya pikir memiliki kode konseptual yang mengimplementasikan std :: forward dapat menambah diskusi. Ini adalah slide dari Scott Meyers talk An Effective C ++ 11/14 Sampler

penerapan kode konseptual std :: forward

Fungsi movedalam kode tersebut adalah std::move. Ada implementasi (berfungsi) untuk itu sebelumnya dalam pembicaraan itu. Saya menemukan implementasi aktual std :: forward di libstdc ++ , di file move.h, tetapi sama sekali tidak instruktif.

Dari sudut pandang pengguna, artinya std::forwardadalah pemeran bersyarat untuk nilai. Ini bisa berguna jika saya menulis fungsi yang mengharapkan nilai atau nilai dalam parameter dan ingin meneruskannya ke fungsi lain sebagai nilai hanya jika diteruskan sebagai nilai. Jika saya tidak membungkus parameter dalam std :: forward, itu akan selalu diberikan sebagai referensi normal.

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

Cukup yakin, itu mencetak

std::string& version
std::string&& version

Kode didasarkan pada contoh dari pembicaraan yang disebutkan sebelumnya. Geser 10, sekitar pukul 15:00 dari awal.

pengguna7610
sumber
2
Tautan kedua Anda akhirnya menunjuk ke suatu tempat yang sama sekali berbeda.
Pharap
34

Dalam penerusan yang sempurna, std :: forward digunakan untuk mengonversi referensi rvalue bernama t1 dan t2 menjadi rujukan nilai tak bernama. Apa tujuan melakukan itu? Bagaimana hal itu mempengaruhi fungsi yang disebut inner jika kita membiarkan t1 & t2 sebagai lvalue?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

Jika Anda menggunakan referensi nilai yang dinamai dalam ekspresi itu sebenarnya adalah nilai (karena Anda merujuk ke objek dengan nama). Perhatikan contoh berikut:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Sekarang, jika kita memanggil outerseperti ini

outer(17,29);

kami ingin 17 dan 29 diteruskan ke # 2 karena 17 dan 29 adalah literal bilangan bulat dan dengan demikian nilainya. Tetapi karena t1dan t2dalam ekspresi inner(t1,t2);adalah nilai, Anda akan memanggil # 1 alih-alih # 2. Itu sebabnya kita perlu mengubah referensi kembali menjadi referensi tanpa nama std::forward. Jadi, t1in outerselalu merupakan ekspresi lvalue sementara forward<T1>(t1)mungkin merupakan ekspresi rvalue tergantung pada T1. Yang terakhir hanya ekspresi lvalue jika T1merupakan referensi lvalue. Dan T1hanya dideduksi menjadi referensi nilai jika argumen pertama ke luar adalah ekspresi nilai.

sellibitze
sumber
Ini adalah semacam penjelasan yang dipermudah, tetapi penjelasan yang dilakukan dengan sangat baik dan fungsional. Orang-orang harus membaca jawaban ini terlebih dahulu dan kemudian pergi lebih dalam jika diinginkan
NicoBerrogorry
@sellibitze Satu lagi pertanyaan, pernyataan mana yang benar ketika menyimpulkan int a; f (a): "karena a adalah nilai, maka int (T&&) sama dengan int (int & &&)" atau "untuk membuat T&& sama dengan int &, jadi T harus int & "? Saya lebih suka yang terakhir.
John
11

Bagaimana hal itu memengaruhi fungsi yang disebut inner jika kita membiarkan t1 & t2 sebagai lvalue?

Jika, setelah membuat instance, T1adalah tipe char, dan T2kelas, Anda ingin lulus t1per salinan dan t2per constreferensi. Nah, kecuali jika inner()mengambilnya per non- constreferensi, yaitu, dalam hal ini Anda juga ingin melakukannya.

Cobalah untuk menulis seperangkat outer()fungsi yang mengimplementasikan ini tanpa referensi nilai, menyimpulkan cara yang tepat untuk meneruskan argumen dari inner()tipe. Saya pikir Anda akan membutuhkan sesuatu 2 ^ 2 dari mereka, cukup template-meta hal yang lumayan untuk menyimpulkan argumen, dan banyak waktu untuk mendapatkan ini dengan benar untuk semua kasus.

Dan kemudian seseorang datang dengan inner()argumen yang mengambil argumen per pointer. Saya pikir sekarang menjadi 3 ^ 2. (Atau 4 ^ 2. Sial, aku tidak mau repot untuk berpikir apakah constpointer akan membuat perbedaan.)

Dan kemudian bayangkan Anda ingin melakukan ini untuk lima parameter. Atau tujuh.

Sekarang Anda tahu mengapa beberapa pikiran cerdas muncul dengan "penerusan yang sempurna": Itu membuat kompiler melakukan semua ini untuk Anda.

sbi
sumber
5

Poin yang belum dibuat sejernih kristal adalah yang static_cast<T&&>menangani const T&dengan benar juga.
Program:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

Menghasilkan:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

Perhatikan bahwa 'f' harus menjadi fungsi templat. Jika hanya didefinisikan sebagai 'void f (int && a)' ini tidak berfungsi.

lawawawa
sumber
Poin yang bagus, jadi T&& dalam pemeran statis juga mengikuti aturan runtuh referensi, bukan?
barney
3

Mungkin bermanfaat untuk menekankan bahwa forward harus digunakan bersama dengan metode luar dengan forwarding / referensi universal. Menggunakan maju dengan sendirinya sebagai pernyataan berikut diizinkan, tetapi tidak ada gunanya selain menyebabkan kebingungan. Komite standar mungkin ingin menonaktifkan fleksibilitas seperti itu jika tidak, mengapa kita tidak menggunakan static_cast saja?

     std::forward<int>(1);
     std::forward<std::string>("Hello");

Menurut pendapat saya, bergerak dan maju adalah pola desain yang merupakan hasil alami setelah tipe referensi r-nilai diperkenalkan. Kita tidak boleh menyebutkan metode dengan asumsi itu digunakan dengan benar kecuali dilarang penggunaan yang salah.

colin
sumber
Saya tidak berpikir bahwa komite C ++ merasa tanggung jawabnya ada pada mereka untuk menggunakan idiom bahasa "dengan benar", atau bahkan mendefinisikan apa "benar" penggunaan itu (meskipun mereka tentu dapat memberikan panduan). Untuk itu, sementara guru, bos, dan teman seseorang mungkin memiliki tugas untuk mengarahkan mereka dengan satu atau lain cara, saya percaya komite C ++ (dan karenanya standar) tidak memiliki tugas itu.
SirGuy
Ya, saya baru saja membaca N2951 dan saya setuju bahwa komite standar tidak memiliki kewajiban untuk menambahkan batasan yang tidak perlu mengenai penggunaan fungsi. Tetapi nama-nama kedua templat fungsi ini (bergerak dan maju) memang agak membingungkan melihat hanya definisi mereka dalam file perpustakaan atau dokumentasi standar (23.2.5 Maju / pindahkan pembantu). Contoh-contoh dalam standar pasti membantu memahami konsep, tetapi mungkin berguna untuk menambahkan lebih banyak komentar untuk membuat hal-hal sedikit lebih jelas.
colin