Untuk memahami mengapa ini adalah pola yang baik, kita harus memeriksa alternatifnya, baik di C ++ 03 dan di C ++ 11.
Kami memiliki metode C ++ 03 untuk mengambil std::string const&
:
struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
dalam hal ini, akan selalu ada satu salinan yang dilakukan. Jika Anda membuat dari string C mentah, a std::string
akan dibangun, lalu disalin lagi: dua alokasi.
Ada metode C ++ 03 untuk mengambil referensi ke a std::string
, lalu menukarnya menjadi lokal std::string
:
struct S
{
std::string data;
S(std::string& str)
{
std::swap(data, str);
}
};
itu adalah "semantik bergerak" versi C ++ 03, dan swap
sering kali dapat dioptimalkan agar sangat murah untuk dilakukan (seperti a move
). Ini juga harus dianalisis dalam konteks:
S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal
dan memaksa Anda untuk membentuk non-sementara std::string
, lalu membuangnya. (Sementara std::string
tidak dapat mengikat ke referensi non-const). Namun, hanya satu alokasi yang dilakukan. Versi C ++ 11 akan mengambil &&
dan meminta Anda untuk memanggilnya dengan std::move
, atau dengan sementara: ini mengharuskan pemanggil secara eksplisit membuat salinan di luar panggilan, dan memindahkan salinan itu ke dalam fungsi atau konstruktor.
struct S
{
std::string data;
S(std::string&& str): data(std::move(str))
{}
};
Menggunakan:
S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal
Selanjutnya, kita dapat melakukan versi C ++ 11 lengkap, yang mendukung penyalinan dan move
:
struct S
{
std::string data;
S(std::string const& str) : data(str) {} // lvalue const, copy
S(std::string && str) : data(std::move(str)) {} // rvalue, move
};
Kami kemudian dapat memeriksa bagaimana ini digunakan:
S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data
std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data
std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data
Cukup jelas bahwa teknik kelebihan beban 2 ini setidaknya sama efisiennya, jika tidak lebih efisien, daripada dua gaya C ++ 03 di atas. Saya akan menjuluki versi 2-kelebihan ini sebagai versi "paling optimal".
Sekarang, kita akan memeriksa versi take-by-copy:
struct S2 {
std::string data;
S2( std::string arg ):data(std::move(x)) {}
};
di setiap skenario tersebut:
S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data
std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data
std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data
Jika Anda membandingkan ini secara berdampingan dengan versi "paling optimal", kami melakukan satu tambahan move
! Tidak sekali kami melakukan ekstra copy
.
Jadi jika kami menganggap itu move
murah, versi ini memberi kami kinerja yang hampir sama dengan versi paling optimal, tetapi kode 2 kali lebih sedikit.
Dan jika Anda mengambil 2 hingga 10 argumen, pengurangan kode adalah eksponensial - 2x kali lebih kecil dengan 1 argumen, 4x dengan 2, 8x dengan 3, 16x dengan 4, 1024x dengan 10 argumen.
Sekarang, kita bisa menyiasatinya melalui penerusan sempurna dan SFINAE, memungkinkan Anda untuk menulis satu konstruktor atau template fungsi yang membutuhkan 10 argumen, melakukan SFINAE untuk memastikan bahwa argumen memiliki jenis yang sesuai, dan kemudian memindahkan-atau-menyalinnya ke dalam negara bagian lokal sesuai kebutuhan. Meskipun hal ini mencegah masalah ukuran program yang bertambah ribuan kali lipat, masih ada tumpukan fungsi yang dihasilkan dari template ini. (Instansiasi fungsi template menghasilkan fungsi)
Dan banyak fungsi yang dihasilkan berarti ukuran kode yang dapat dieksekusi lebih besar, yang dengan sendirinya dapat mengurangi kinerja.
Dengan biaya beberapa move
detik, kita mendapatkan kode yang lebih pendek dan kinerja yang hampir sama, dan seringkali lebih mudah untuk memahami kode.
Sekarang, ini hanya berfungsi karena kita tahu, ketika fungsi (dalam hal ini, konstruktor) dipanggil, bahwa kita akan menginginkan salinan lokal dari argumen itu. Idenya adalah jika kita tahu bahwa kita akan membuat salinan, kita harus memberi tahu penelepon bahwa kita sedang membuat salinan dengan memasukkannya ke dalam daftar argumen kita. Mereka kemudian dapat mengoptimalkan sekitar fakta bahwa mereka akan memberi kita salinannya (dengan beralih ke argumen kita, misalnya).
Keuntungan lain dari teknik 'ambil dengan nilai "adalah bahwa sering memindahkan konstruktor tidak terkecuali. Itu berarti fungsi yang mengambil nilai demi dan keluar dari argumennya sering kali tidak terkecuali, memindahkan apa pun throw
keluar dari tubuhnya dan ke dalam lingkup pemanggilan (yang kadang-kadang dapat menghindarinya melalui konstruksi langsung, atau membangun item dan move
menjadi argumen, untuk mengontrol di mana terjadi lemparan) Membuat metode nothrow seringkali sepadan.