Apakah pass-by-value merupakan default yang wajar di C ++ 11?

142

Dalam C ++ tradisional, meneruskan nilai ke fungsi dan metode lambat untuk objek besar, dan umumnya disukai. Alih-alih, programmer C ++ cenderung meneruskan referensi, yang lebih cepat, tetapi yang memperkenalkan segala macam pertanyaan rumit seputar kepemilikan dan terutama seputar manajemen memori (jika objek dialokasikan dialokasikan)

Sekarang, di C ++ 11, kami memiliki referensi nilai dan memindahkan konstruktor, yang berarti bahwa mungkin untuk mengimplementasikan objek besar (seperti std::vector) yang murah untuk melewati nilai ke dalam dan keluar dari suatu fungsi.

Jadi, apakah ini berarti bahwa standarnya harus lewat nilai untuk contoh jenis seperti std::vectordan std::string? Bagaimana dengan objek kustom? Apa praktik terbaik yang baru?

Derek Thurn
sumber
22
pass by reference ... which introduces all sorts of complicated questions around ownership and especially around memory management (in the event that the object is heap-allocated). Saya tidak mengerti bagaimana rumit atau bermasalah untuk kepemilikan? Mungkin saya melewatkan sesuatu?
iammilind
1
@ iammilind: Contoh dari pengalaman pribadi. Satu utas memiliki objek string. Ini diteruskan ke fungsi yang memunculkan thread lain, tetapi tidak diketahui oleh pemanggil fungsi mengambil string sebagai const std::string&dan bukan salinan. Utas pertama kemudian keluar ...
Zan Lynx
12
@ ZanLynx: Kedengarannya seperti fungsi yang jelas tidak pernah dirancang untuk dipanggil sebagai fungsi utas.
Nicol Bolas
5
Setuju dengan iammilind, saya tidak melihat masalah. Melewati dengan referensi const harus menjadi default Anda untuk objek "besar", dan dengan nilai untuk objek yang lebih kecil. Saya akan menempatkan batas antara besar dan kecil sekitar 16 byte (atau 4 pointer pada sistem 32 bit).
JN
3
Kembali Sutter Herb ke Dasar-Dasar! Hal-hal penting dari presentasi C ++ Modern di CppCon membahas sedikit tentang hal ini. Video di sini .
Chris Drew

Jawaban:

138

Ini adalah standar yang masuk akal jika Anda perlu membuat salinan di dalam tubuh. Ini adalah apa Dave Abrahams menganjurkan :

Pedoman: Jangan menyalin argumen fungsi Anda. Sebagai gantinya, berikan nilai dan biarkan kompiler melakukan penyalinan.

Dalam kode ini berarti jangan lakukan ini:

void foo(T const& t)
{
    auto copy = t;
    // ...
}

tetapi lakukan ini:

void foo(T t)
{
    // ...
}

yang memiliki keuntungan yang bisa digunakan oleh penelepon fooseperti:

T lval;
foo(lval); // copy from lvalue
foo(T {}); // (potential) move from prvalue
foo(std::move(lval)); // (potential) move from xvalue

dan hanya sedikit pekerjaan yang dilakukan. Anda perlu dua kelebihan untuk melakukan hal yang sama dengan referensi, void foo(T const&);dan void foo(T&&);.

Dengan mengingat hal itu, saya sekarang menulis konstruktor berharga saya seperti:

class T {
    U u;
    V v;
public:
    T(U u, V v)
        : u(std::move(u))
        , v(std::move(v))
    {}
};

Kalau tidak, melewati referensi constmasih masuk akal.

Luc Danton
sumber
29
+1, terutama untuk bit terakhir :) Orang tidak boleh lupa bahwa Move Constructors hanya dapat dipanggil jika objek yang akan dipindahkan tidak diharapkan tidak berubah sesudahnya: SomeProperty p; for (auto x: vec) { x.foo(p); }tidak cocok, misalnya. Juga, Pindah Konstruktor memiliki biaya (semakin besar objek, semakin tinggi biaya) sementara const&pada dasarnya gratis.
Matthieu M.
25
@ MatthieuM. Tetapi penting untuk mengetahui apa "semakin besar objek, semakin tinggi biaya" dari langkah yang sebenarnya berarti: "lebih besar" sebenarnya berarti "semakin banyak variabel anggota". Sebagai contoh, memindahkan satu std::vectordengan sejuta elemen berharga sama dengan memindahkan satu dengan lima elemen karena hanya penunjuk ke array pada tumpukan yang dipindahkan, tidak setiap objek dalam vektor. Jadi sebenarnya bukan masalah besar.
Lucas
+1 Saya juga cenderung menggunakan konstruk pass-by-value-then-move sejak saya mulai menggunakan C ++ 11. Ini membuat saya merasa agak tidak nyaman, karena kode saya sekarang ada di std::movesemua tempat ..
stijn
1
Ada satu risiko dengan const&, yang telah membuat saya tersandung beberapa kali. void foo(const T&); int main() { S s; foo(s); }. Ini dapat dikompilasi, meskipun jenisnya berbeda, jika ada konstruktor T yang menggunakan S sebagai argumen. Ini bisa lambat, karena objek T besar mungkin sedang dibangun. Anda mungkin berpikir Anda melewatkan referensi tanpa menyalin, tetapi mungkin Anda memang demikian. Lihat jawaban ini untuk pertanyaan yang saya minta lebih banyak. Pada dasarnya, &biasanya hanya mengikat nilai-nilai, tetapi ada pengecualian untuk rvalue. Ada beberapa alternatif.
Aaron McDaid
1
@AaronMcDaid Ini adalah berita lama, dalam arti itu adalah sesuatu yang Anda harus selalu waspada bahkan sebelum C ++ 11. Dan tidak banyak yang berubah sehubungan dengan itu.
Luc Danton
71

Dalam hampir semua kasus, semantik Anda harus berupa:

bar(foo f); // want to obtain a copy of f
bar(const foo& f); // want to read f
bar(foo& f); // want to modify f

Semua tanda tangan lainnya harus digunakan hanya dengan hemat, dan dengan justifikasi yang baik. Kompiler sekarang akan selalu bekerja dengan cara yang paling efisien. Anda bisa terus menulis kode Anda!

Ayjay
sumber
2
Meskipun saya lebih suka melewatkan pointer jika saya akan memodifikasi argumen. Saya setuju dengan panduan gaya Google bahwa ini membuatnya lebih jelas bahwa argumen akan dimodifikasi tanpa perlu memeriksa tanda tangan fungsi ( google-styleguide.googlecode.com/svn/trunk/… ).
Max Lybbert
40
Alasan mengapa saya tidak menyukai pointer yang lewat adalah karena ia menambah kemungkinan kegagalan fungsi saya. Saya mencoba untuk menulis semua fungsi saya sehingga mereka terbukti benar, karena sangat mengurangi ruang bagi bug untuk bersembunyi. foo(bar& x) { x.a = 3; }Heck of a jauh lebih dapat diandalkan (dan dapat dibaca!) foo(bar* x) {if (!x) throw std::invalid_argument("x"); x->a = 3;
Daripada
22
@ Max Lybbert: Dengan parameter pointer, Anda tidak perlu memeriksa tanda tangan fungsi, tetapi Anda perlu memeriksa dokumentasi untuk mengetahui apakah Anda diizinkan untuk melewati null pointer, jika fungsi tersebut akan mengambil kepemilikan, dll. IMHO, parameter pointer menyampaikan informasi yang jauh lebih sedikit daripada referensi non-const. Namun saya setuju bahwa akan lebih baik jika memiliki petunjuk visual di situs panggilan agar argumennya dapat dimodifikasi (seperti refkata kunci dalam C #).
Luc Touraille
Berkenaan dengan melewati nilai dan mengandalkan semantik bergerak, saya merasa ketiga pilihan ini melakukan pekerjaan yang lebih baik dalam menjelaskan tujuan penggunaan parameter. Ini adalah pedoman yang selalu saya ikuti juga.
Trevor Hickey
1
@AaronMcDaid is shared_ptr intended to never be null? Much as (I think) unique_ptr is?Kedua asumsi itu salah. unique_ptrdan shared_ptrdapat menyimpan null / nullptrnilai. Jika Anda tidak ingin khawatir tentang nilai nol, Anda harus menggunakan referensi, karena mereka tidak akan pernah menjadi nol. Anda juga tidak perlu mengetik ->, yang menurut Anda menyebalkan :)
Julian
10

Lewati parameter dengan nilai jika di dalam fungsi tubuh Anda memerlukan salinan objek atau hanya perlu memindahkan objek. Lewati const&jika Anda hanya perlu akses non-mutasi ke objek.

Contoh copy objek:

void copy_antipattern(T const& t) { // (Don't do this.)
    auto copy = t;
    t.some_mutating_function();
}

void copy_pattern(T t) { // (Do this instead.)
    t.some_mutating_function();
}

Contoh pemindahan objek:

std::vector<T> v; 

void move_antipattern(T const& t) {
    v.push_back(t); 
}

void move_pattern(T t) {
    v.push_back(std::move(t)); 
}

Contoh akses non-mutasi:

void read_pattern(T const& t) {
    t.some_const_function();
}

Untuk alasannya, lihat posting blog ini oleh Dave Abrahams dan Xiang Fan .

Edward Brey
sumber
0

Tanda tangan suatu fungsi harus mencerminkan penggunaan yang dimaksudkan. Keterbacaan penting, juga untuk pengoptimal.

Ini adalah prasyarat terbaik bagi pengoptimal untuk membuat kode tercepat - setidaknya dalam teori dan jika tidak dalam kenyataan maka dalam beberapa tahun kenyataan.

Pertimbangan kinerja seringkali dinilai berlebihan dalam konteks parameter yang lewat. Penerusan yang sempurna adalah contohnya. Fungsi seperti emplace_backkebanyakan sangat pendek dan inline.

Patrick Fromberg
sumber