Saya seorang pemula C ++ tetapi bukan pemula pemrograman. Saya mencoba mempelajari C ++ (c ++ 11) dan bagi saya hal yang paling tidak jelas adalah parameter yang lewat.
Saya mempertimbangkan contoh sederhana ini:
Kelas yang semua anggotanya memiliki tipe primitif:
CreditCard(std::string number, int expMonth, int expYear,int pin):number(number), expMonth(expMonth), expYear(expYear), pin(pin)
Kelas yang memiliki tipe primitif sebagai anggota + 1 tipe kompleks:
Account(std::string number, float amount, CreditCard creditCard) : number(number), amount(amount), creditCard(creditCard)
Kelas yang memiliki tipe primitif sebagai anggota + 1 kumpulan dari beberapa tipe kompleks:
Client(std::string firstName, std::string lastName, std::vector<Account> accounts):firstName(firstName), lastName(lastName), accounts(accounts)
Saat saya membuat akun, saya melakukan ini:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc);
Jelas kartu kredit akan disalin dua kali dalam skenario ini. Jika saya menulis ulang konstruktor itu sebagai
Account(std::string number, float amount, CreditCard& creditCard)
: number(number)
, amount(amount)
, creditCard(creditCard)
akan ada satu salinan. Jika saya menulis ulang sebagai
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number)
, amount(amount)
, creditCard(std::forward<CreditCard>(creditCard))
Akan ada 2 gerakan dan tidak ada salinan.
Saya pikir terkadang Anda mungkin ingin menyalin beberapa parameter, terkadang Anda tidak ingin menyalin saat Anda membuat objek itu.
Saya berasal dari C # dan, digunakan untuk referensi, itu agak aneh bagi saya dan saya pikir harus ada 2 kelebihan beban untuk setiap parameter tetapi saya tahu saya salah.
Apakah ada praktik terbaik tentang cara mengirim parameter dalam C ++ karena saya benar-benar menemukannya, katakanlah, tidak sepele. Bagaimana Anda menangani contoh saya yang disajikan di atas?
std::string
adalah kelas, sama sepertiCreditCard
, bukan tipe primitif."abc"
bukanlah tipestd::string
, bukan tipechar */const char *
, tetapi tipeconst char[N]
(dengan N = 4 dalam kasus ini karena tiga karakter dan null). Itu bagus, kesalahpahaman umum untuk disingkirkan.Jawaban:
PERTANYAAN PALING PENTING PERTAMA:
Jika fungsi Anda perlu mengubah objek asli yang sedang diteruskan, sehingga setelah panggilan kembali, modifikasi pada objek itu akan terlihat oleh pemanggil, Anda harus meneruskan referensi lvalue :
Jika fungsi Anda tidak perlu memodifikasi objek aslinya, dan tidak perlu membuat salinannya (dengan kata lain, hanya perlu mengamati statusnya), maka Anda harus meneruskan referensi lvalue ke
const
:Ini akan memungkinkan Anda untuk memanggil fungsi baik dengan lvalues (lvalues adalah objek dengan identitas stabil) dan dengan rvalues (rvalues adalah, misalnya temporaries , atau objek yang akan Anda pindahkan sebagai hasil pemanggilan
std::move()
).Seseorang juga dapat berargumen bahwa untuk tipe atau tipe fundamental yang penyalinannya cepat , seperti
int
,bool
, atauchar
, tidak perlu lewat referensi jika fungsi hanya perlu mengamati nilai, dan lewat nilai harus disukai . Itu benar jika semantik referensi tidak diperlukan, tetapi bagaimana jika fungsi ingin menyimpan pointer ke objek input yang sama di suatu tempat, sehingga pembacaan di masa depan melalui pointer itu akan melihat modifikasi nilai yang telah dilakukan di beberapa bagian lain dari kode? Dalam hal ini, melewatkan referensi adalah solusi yang tepat.Jika fungsi Anda tidak perlu mengubah objek asli, tetapi perlu menyimpan salinan objek itu ( mungkin untuk mengembalikan hasil transformasi input tanpa mengubah input ), maka Anda dapat mempertimbangkan untuk mengambil berdasarkan nilai :
Memanggil fungsi di atas akan selalu menghasilkan satu salinan saat meneruskan nilai l, dan satu salinan saat meneruskan nilai r. Jika fungsi Anda perlu untuk menyimpan objek di suatu tempat ini, Anda bisa melakukan tambahan langkah darinya (misalnya, dalam kasus
foo()
ini adalah fungsi anggota yang perlu menyimpan nilai dalam anggota data ).Dalam kasus bergerak mahal untuk objek jenis
my_class
, maka Anda dapat mempertimbangkan untuk membebanifoo()
dan memberikan satu versi untuk lvalues (menerima referensi lvalueconst
) dan satu versi untuk rvalues (menerima referensi rvalue):Fungsi di atas sangat mirip, pada kenyataannya, Anda bisa membuat satu fungsi darinya:
foo()
bisa menjadi template fungsi dan Anda bisa menggunakan penerusan sempurna untuk menentukan apakah perpindahan atau salinan objek yang diteruskan akan dihasilkan secara internal:Anda mungkin ingin mempelajari lebih lanjut tentang desain ini dengan menonton ceramah oleh Scott Meyers ini (ingatlah fakta bahwa istilah " Referensi Universal " yang dia gunakan tidak standar).
Satu hal yang perlu diingat adalah bahwa
std::forward
biasanya akan berakhir di a perpindahan nilai r, jadi meskipun terlihat relatif tidak berbahaya, meneruskan objek yang sama beberapa kali dapat menjadi sumber masalah - misalnya, berpindah dari objek yang sama dua kali! Jadi berhati-hatilah untuk tidak mengulanginya, dan jangan meneruskan argumen yang sama beberapa kali dalam pemanggilan fungsi:Perhatikan juga, bahwa Anda biasanya tidak menggunakan solusi berbasis template kecuali Anda memiliki alasan kuat untuk itu, karena membuat kode Anda lebih sulit untuk dibaca. Biasanya, Anda harus fokus pada kejelasan dan kesederhanaan .
Di atas hanyalah pedoman sederhana, tetapi sebagian besar waktu itu akan mengarahkan Anda ke keputusan desain yang baik.
TENTANG SISA POSTING ANDA:
Ini tidak benar. Untuk memulainya, referensi rvalue tidak bisa mengikat ke lvalue, jadi ini hanya akan dikompilasi ketika Anda meneruskan rvalue type
CreditCard
ke konstruktor Anda. Misalnya:Tetapi itu tidak akan berhasil jika Anda mencoba melakukan ini:
Karena
cc
adalah nilai l dan referensi nilai r tidak dapat mengikat ke nilai l. Selain itu, saat mengikat referensi ke suatu objek, tidak ada pemindahan yang dilakukan : itu hanya mengikat referensi. Jadi, hanya akan ada satu gerakan.Jadi berdasarkan pedoman yang diberikan di bagian pertama jawaban ini, jika Anda khawatir dengan jumlah gerakan yang dihasilkan saat Anda mengambil
CreditCard
nilai by, Anda dapat menentukan dua overload konstruktor, satu mengambil referensi lvalue keconst
(CreditCard const&
) dan satu pengambilan referensi rvalue (CreditCard&&
).Resolusi kelebihan beban akan memilih yang pertama saat meneruskan nilai l (dalam hal ini, satu salinan akan dilakukan) dan yang terakhir saat meneruskan nilai r (dalam hal ini, satu gerakan akan dilakukan).
Penggunaan Anda
std::forward<>
biasanya terlihat saat Anda ingin mencapai penerusan sempurna . Dalam hal ini, konstruktor Anda sebenarnya adalah template konstruktor , dan akan terlihat kurang lebih seperti berikutDalam arti tertentu, ini menggabungkan kedua overload yang telah saya tunjukkan sebelumnya menjadi satu fungsi tunggal:
C
akan disimpulkanCreditCard&
jika Anda meneruskan nilai l, dan karena referensi aturan menciutkan, ini akan menyebabkan fungsi ini dibuat:Hal ini akan menyebabkan copy-konstruksi dari
creditCard
, seperti yang Anda inginkan. Di sisi lain, ketika nilai r dilewatkan,C
akan disimpulkan menjadiCreditCard
, dan fungsi ini akan dipakai sebagai gantinya:Hal ini akan menyebabkan langkah-konstruksi dari
creditCard
, yang adalah apa yang Anda inginkan (karena nilai yang berlalu adalah nilai p, dan itu berarti kami berwenang untuk berpindah dari itu).sumber
const
. Jika Anda perlu mengubah objek asli, gunakan ref ke non-`const. Jika Anda perlu membuat salinan dan bergerak murah, ambil berdasarkan nilai dan kemudian pindah.obj
alih-alih membuat salinan lokal untuk dipindahkan, bukan?std::forward
dapat dipanggil hanya sekali . Saya telah melihat orang-orang memasukkannya ke dalam loop, dll. Dan karena jawaban ini akan dilihat oleh banyak pemula, IMHO harus ada label "Peringatan!" Yang gemuk untuk membantu mereka menghindari jebakan ini.std::is_constructible<>
sifat tipe kecuali jika mereka benar-benar SFINAE- dibatasi - yang mungkin tidak sepele bagi sebagian orang.Pertama, izinkan saya mengoreksi beberapa detail. Saat Anda mengucapkan yang berikut:
Itu salah. Mengikat ke referensi nilai r bukanlah suatu gerakan. Hanya ada satu gerakan.
Selain itu, karena
CreditCard
bukan parameter template,std::forward<CreditCard>(creditCard)
ini hanyalah cara untuk mengatakannya secara verbosestd::move(creditCard)
.Sekarang...
Jika tipe Anda memiliki gerakan "murah", Anda mungkin ingin membuat hidup Anda mudah dan mengambil semuanya dengan nilai dan "
std::move
bersama".Pendekatan ini akan menghasilkan dua gerakan jika hanya menghasilkan satu, tetapi jika gerakan itu murah, mungkin bisa diterima.
Sementara kita berada pada masalah "langkah murah", saya harus mengingatkan Anda itu
std::string
membahas ini sering diterapkan dengan apa yang disebut pengoptimalan string kecil, jadi gerakannya mungkin tidak semurah menyalin beberapa petunjuk. Seperti biasa dengan masalah pengoptimalan, apakah penting atau tidak adalah sesuatu yang ditanyakan kepada profiler Anda, bukan saya.Apa yang harus dilakukan jika Anda tidak ingin melakukan gerakan ekstra itu? Mungkin mereka terbukti terlalu mahal, atau lebih buruk, mungkin jenisnya tidak dapat benar-benar dipindahkan dan Anda mungkin dikenakan salinan tambahan.
Jika hanya ada satu parameter yang bermasalah, Anda dapat memberikan dua kelebihan beban, dengan
T const&
danT&&
. Itu akan mengikat referensi sepanjang waktu hingga inisialisasi anggota yang sebenarnya, di mana salinan atau pemindahan terjadi.Namun, jika Anda memiliki lebih dari satu parameter, hal ini menyebabkan ledakan eksponensial dalam jumlah kelebihan beban.
Ini adalah masalah yang bisa diselesaikan dengan penerusan sempurna. Itu berarti Anda menulis template sebagai gantinya, dan gunakan
std::forward
untuk membawa kategori nilai argumen ke tujuan akhirnya sebagai anggota.sumber
Account("",0,{brace, initialisation})
lagi.Pertama-tama,
std::string
adalah tipe kelas yang lumayan besarstd::vector
. Ini jelas tidak primitif.Jika Anda mengambil tipe besar yang dapat dipindahkan berdasarkan nilai ke dalam konstruktor, saya akan
std::move
memasukkannya ke anggota:Ini persis bagaimana saya akan merekomendasikan menerapkan konstruktor. Ini menyebabkan anggota
number
dancreditCard
untuk dipindahkan dibangun, daripada salinan dibangun. Saat Anda menggunakan konstruktor ini, akan ada satu salinan (atau pindahkan, jika sementara) saat objek diteruskan ke konstruktor dan kemudian satu gerakan saat menginisialisasi anggota.Sekarang mari pertimbangkan konstruktor ini:
Anda benar, ini akan melibatkan satu salinan
creditCard
, karena ini pertama kali diteruskan ke konstruktor melalui referensi. Tapi sekarang Anda tidak bisa meneruskanconst
objek ke konstruktor (karena referensinya non-const
) dan Anda tidak bisa meneruskan objek sementara. Misalnya, Anda tidak dapat melakukan ini:Sekarang mari pertimbangkan:
Di sini Anda telah menunjukkan kesalahpahaman tentang referensi rvalue dan
std::forward
. Anda seharusnya hanya benar-benar menggunakanstd::forward
ketika objek yang Anda teruskan dideklarasikan sebagaiT&&
beberapa tipe deduksiT
. Di siniCreditCard
tidak disimpulkan (saya berasumsi), sehinggastd::forward
digunakan dalam kesalahan. Cari referensi universal .sumber
Saya menggunakan aturan yang cukup sederhana untuk kasus umum: Gunakan salinan untuk POD (int, bool, double, ...) dan const & untuk yang lainnya ...
Dan mau menyalin atau tidak, tidak dijawab oleh metode tanda tangan tetapi lebih oleh apa yang Anda lakukan dengan paramaters.
presisi untuk penunjuk: Saya hampir tidak pernah menggunakannya. Hanya keuntungan dari & adalah bahwa mereka dapat menjadi nol, atau ditugaskan kembali.
sumber
(*) pointer mungkin mengacu pada memori yang dialokasikan secara dinamis, oleh karena itu jika memungkinkan Anda harus lebih memilih referensi daripada pointer bahkan jika referensi, pada akhirnya, biasanya diimplementasikan sebagai pointer.
(**) "normal" berarti dengan konstruktor salinan (jika Anda mengirimkan objek dengan tipe parameter yang sama) atau dengan konstruktor normal (jika Anda meneruskan tipe yang kompatibel untuk kelas). Ketika Anda mengirimkan sebuah objek
myMethod(std::string)
, misalnya, salinan konstruktor akan digunakan jika sebuahstd::string
dilewatkan, oleh karena itu Anda harus memastikan bahwa objek itu ada.sumber