Bagaimana cara "mengembalikan objek" di C ++?

167

Saya tahu judulnya terdengar akrab karena ada banyak pertanyaan serupa, tetapi saya meminta aspek masalah yang berbeda (saya tahu perbedaan antara memiliki barang-barang di tumpukan dan meletakkannya di tumpukan).

Di Jawa saya selalu bisa mengembalikan referensi ke objek "lokal"

public Thing calculateThing() {
    Thing thing = new Thing();
    // do calculations and modify thing
    return thing;
}

Di C ++, untuk melakukan sesuatu yang serupa saya punya 2 pilihan

(1) Saya dapat menggunakan referensi kapan pun saya perlu "mengembalikan" suatu objek

void calculateThing(Thing& thing) {
    // do calculations and modify thing
}

Kemudian gunakan seperti ini

Thing thing;
calculateThing(thing);

(2) Atau saya dapat mengembalikan pointer ke objek yang dialokasikan secara dinamis

Thing* calculateThing() {
    Thing* thing(new Thing());
    // do calculations and modify thing
    return thing;
}

Kemudian gunakan seperti ini

Thing* thing = calculateThing();
delete thing;

Menggunakan pendekatan pertama saya tidak perlu membebaskan memori secara manual, tetapi bagi saya itu membuat kode sulit dibaca. Masalah dengan pendekatan kedua adalah, saya harus ingat delete thing;, yang tidak terlihat bagus. Saya tidak ingin mengembalikan nilai yang disalin karena tidak efisien (saya pikir), jadi inilah pertanyaannya

  • Apakah ada solusi ketiga (yang tidak perlu menyalin nilainya)?
  • Apakah ada masalah jika saya tetap pada solusi pertama?
  • Kapan dan mengapa saya harus menggunakan solusi kedua?
phunehehe
sumber
32
+1 untuk mengajukan pertanyaan dengan baik.
Kangkan
1
Menjadi sangat bertele-tele, agak tidak tepat untuk mengatakan bahwa "fungsi mengembalikan sesuatu". Lebih tepatnya, mengevaluasi panggilan fungsi menghasilkan nilai . Nilai selalu merupakan objek (kecuali jika itu adalah fungsi batal). Perbedaannya adalah apakah nilainya glvalue atau prvalue - yang ditentukan oleh apakah tipe pengembalian yang dinyatakan adalah referensi atau tidak.
Kerrek SB

Jawaban:

107

Saya tidak ingin mengembalikan nilai yang disalin karena tidak efisien

Buktikan itu.

Cari RVO dan NRVO, dan dalam C ++ 0x semantik bergerak. Dalam kebanyakan kasus di C ++ 03, parameter keluar hanyalah cara yang baik untuk membuat kode Anda jelek, dan di C ++ 0x Anda sebenarnya akan melukai diri sendiri dengan menggunakan parameter keluar.

Cukup tulis kode bersih, kembalikan dengan nilai. Jika kinerja adalah masalah, buat profil itu (berhenti menebak), dan temukan apa yang dapat Anda lakukan untuk memperbaikinya. Kemungkinan tidak akan mengembalikan sesuatu dari fungsi.


Yang mengatakan, jika Anda sudah mati menulis seperti itu, Anda mungkin ingin melakukan parameter keluar. Ini menghindari alokasi memori dinamis, yang lebih aman dan umumnya lebih cepat. Itu memang mengharuskan Anda memiliki beberapa cara untuk membangun objek sebelum memanggil fungsi, yang tidak selalu masuk akal untuk semua objek.

Jika Anda ingin menggunakan alokasi dinamis, hal yang paling tidak bisa dilakukan adalah memasukkannya ke dalam smart pointer. (Lagipula ini harus dilakukan setiap saat) Maka Anda tidak perlu khawatir untuk menghapus apa pun, hal-hal itu aman dari pengecualian, dll. Satu-satunya masalah adalah kemungkinannya lebih lambat daripada mengembalikan nilainya!

GManNickG
sumber
10
@ phunehehe: Tidak ada titik berspekulasi, Anda harus membuat profil kode Anda dan mencari tahu. (Petunjuk: tidak.) Compiler sangat cerdas, mereka tidak akan membuang waktu menyalin hal-hal di sekitar jika mereka tidak perlu. Sekalipun menyalin sesuatu membutuhkan biaya, Anda tetap harus berusaha mendapatkan kode yang baik daripada kode yang cepat; kode yang baik mudah untuk dioptimalkan ketika kecepatan menjadi masalah. Tidak ada gunanya membuat kode untuk sesuatu yang Anda tidak tahu adalah masalah; terutama jika Anda benar-benar memperlambatnya atau tidak menghasilkan apa-apa darinya. Dan jika Anda menggunakan C ++ 0x, semantik bergerak menjadikan ini bukan masalah.
GManNickG
1
@ GMan, re: RVO: sebenarnya ini hanya benar jika penelepon Anda dan callee kebetulan berada di unit kompilasi yang sama, yang di dunia nyata itu bukan sebagian besar waktu. Jadi, Anda kecewa jika kode Anda tidak semuanya templated (dalam hal ini semua akan berada dalam satu unit kompilasi) atau Anda memiliki beberapa optimasi tautan waktu (GCC hanya memiliki dari 4,5).
Alex B
2
@Alex: Kompiler menjadi lebih baik dan lebih baik dalam mengoptimalkan seluruh unit terjemahan. (VC melakukannya untuk beberapa rilis sekarang.)
sbi
9
@Alex B: Ini benar-benar sampah. Banyak konvensi panggilan yang sangat umum membuat penelepon bertanggung jawab untuk mengalokasikan ruang untuk nilai pengembalian yang besar dan callee yang bertanggung jawab untuk konstruksi mereka. RVO dengan senang hati bekerja di seluruh unit kompilasi bahkan tanpa optimasi waktu tautan.
CB Bailey
6
@ Charles, setelah memeriksa tampaknya benar! Saya menarik pernyataan saya yang salah informasi.
Alex B
41

Cukup buat objek dan kembalikan

Thing calculateThing() {
    Thing thing;
    // do calculations and modify thing
     return thing;
}

Saya pikir Anda akan melakukan kebaikan jika Anda lupa tentang optimasi dan hanya menulis kode yang dapat dibaca (Anda harus menjalankan profiler nanti - tetapi jangan pra-optimalkan).

Amir Rachum
sumber
2
Thing thing();mendeklarasikan fungsi lokal dan mengembalikan a Thing.
dreamlax
2
Hal () menyatakan fungsi mengembalikan sesuatu. Tidak ada benda yang dibangun di tubuh fungsi Anda.
CB Bailey
@dreamlax @Charles @ GMan Agak terlambat, tapi sudah diperbaiki.
Amir Rachum
Bagaimana cara kerjanya di C ++ 98? Saya mendapatkan kesalahan pada juru bahasa CINT dan bertanya-tanya itu karena C ++ 98 atau CINT sendiri ...!
xcorat
16

Cukup kembalikan objek seperti ini:

Thing calculateThing() 
{
   Thing thing();
   // do calculations and modify thing
   return thing;
}

Ini akan meminta copy constructor pada Things, jadi Anda mungkin ingin melakukan implementasi Anda sendiri untuk itu. Seperti ini:

Thing(const Thing& aThing) {}

Ini mungkin melakukan sedikit lebih lambat, tetapi mungkin tidak menjadi masalah sama sekali.

Memperbarui

Compiler mungkin akan mengoptimalkan panggilan ke copy constructor, sehingga tidak akan ada overhead tambahan. (Seperti dreamlax ditunjukkan dalam komentar).

Martin Ingvar Kofoed Jensen
sumber
9
Thing thing();mendeklarasikan fungsi lokal yang mengembalikan a Thing, juga, standar memungkinkan kompiler untuk menghilangkan copy constructor dalam kasus yang Anda sajikan; setiap kompiler modern mungkin akan melakukannya.
dreamlax
1
Anda membawa poin yang baik untuk menerapkan copy constructor, terutama jika salinan yang dalam diperlukan.
mbadawi23
+1 untuk menyatakan secara eksplisit tentang copy constructor, meskipun seperti @dreamlax mengatakan kompiler kemungkinan besar akan "mengoptimalkan" kode kembali untuk fungsi menghindari panggilan yang tidak benar-benar diperlukan untuk copy constructor.
jose.angel.jimenez
Pada 2018, di VS 2017, ia mencoba menggunakan konstruktor pemindahan. Jika memindahkan konstruktor dihapus dan konstruktor salin tidak, itu tidak akan dikompilasi.
Andrew
11

Apakah Anda mencoba menggunakan smart pointer (jika benda itu benar-benar benda besar dan berat), seperti auto_ptr:


std::auto_ptr<Thing> calculateThing()
{
  std::auto_ptr<Thing> thing(new Thing);
  // .. some calculations
  return thing;
}


// ...
{
  std::auto_ptr<Thing> thing = calculateThing();
  // working with thing

  // auto_ptr frees thing 
}
demetrios
sumber
4
auto_ptrs sudah usang; gunakan shared_ptratau unique_ptrsebagai gantinya.
MBraedley
Hanya akan menambahkan ini di sini ... Saya telah menggunakan c ++ selama bertahun-tahun, meskipun tidak secara profesional dengan c ++ ... Saya sudah bertekad untuk mencoba tidak menggunakan smart pointer lagi, mereka hanya kekacauan imo mutlak dan menyebabkan semua macam masalah, tidak terlalu membantu untuk mempercepat kode banyak. Saya lebih suka hanya menyalin data sekitar dan mengelola pointer sendiri, menggunakan RAII. Jadi saya menyarankan bahwa jika Anda bisa, hindari pointer pintar.
Andrew
8

Salah satu cara cepat untuk menentukan apakah copy constructor dipanggil adalah dengan menambahkan logging ke copy constructor kelas Anda:

MyClass::MyClass(const MyClass &other)
{
    std::cout << "Copy constructor was called" << std::endl;
}

MyClass someFunction()
{
    MyClass dummy;
    return dummy;
}

Panggilan someFunction; jumlah baris "Copy constructor dipanggil" yang akan Anda dapatkan akan bervariasi antara 0, 1, dan 2. Jika Anda tidak mendapatkannya, maka kompiler Anda telah mengoptimalkan nilai pengembalian (yang diizinkan untuk dilakukan). Jika Anda tidak mendapatkan 0, dan pembuat salinan Anda sangat mahal, maka cari cara alternatif untuk mengembalikan instance dari fungsi Anda.

dreamlax
sumber
1

Pertama Anda memiliki kesalahan dalam kode, maksud Anda miliki Thing *thing(new Thing());, dan hanya return thing;.

  • Gunakan shared_ptr<Thing>. Lakukan itu karena itu adalah pointer. Ini akan dihapus untuk Anda ketika referensi terakhir untuk yang Thingterkandung keluar dari ruang lingkup.
  • Solusi pertama sangat umum di perpustakaan yang naif. Ini memiliki beberapa kinerja, dan overhead sintaksis, hindari jika memungkinkan
  • Gunakan solusi kedua hanya jika Anda dapat menjamin tidak ada pengecualian akan dilemparkan, atau ketika kinerja sangat penting (Anda akan berinteraksi dengan C atau perakitan sebelum ini bahkan menjadi relevan).
Matt Joiner
sumber
0

Saya yakin seorang ahli C ++ akan datang dengan jawaban yang lebih baik, tetapi secara pribadi saya suka pendekatan kedua. Menggunakan smart pointer membantu dengan masalah lupa deletedan seperti yang Anda katakan, itu terlihat lebih bersih daripada harus membuat objek sebelumnya (dan masih harus menghapusnya jika Anda ingin mengalokasikannya di heap).

EMP
sumber