Apakah praktik mengembalikan variabel referensi C ++ jahat?

341

Saya kira ini agak subyektif; Saya tidak yakin apakah pendapatnya akan bulat (saya telah melihat banyak cuplikan kode tempat referensi dikembalikan).

Menurut komentar terhadap pertanyaan ini, saya baru saja bertanya, mengenai inisialisasi referensi , mengembalikan referensi bisa jahat karena, [seperti yang saya mengerti] membuatnya lebih mudah untuk tidak menghapusnya, yang dapat menyebabkan kebocoran memori.

Ini membuat saya khawatir, karena saya telah mengikuti contoh (kecuali saya membayangkan hal-hal) dan melakukan ini di beberapa tempat yang adil ... Apakah saya salah paham? Apakah itu jahat? Kalau begitu, seberapa jahatkah?

Saya merasa bahwa karena kantong campuran pointer dan referensi saya, dikombinasikan dengan fakta bahwa saya baru menggunakan C ++, dan kebingungan total tentang apa yang harus digunakan ketika, aplikasi saya pasti kehabisan memori ...

Juga, saya mengerti bahwa menggunakan pointer pintar / bersama umumnya diterima sebagai cara terbaik untuk menghindari kebocoran memori.

Nick Bolton
sumber
Tidaklah jahat jika Anda menulis fungsi / metode mirip rajin.
John Z. Li

Jawaban:

411

Secara umum, mengembalikan referensi sangat normal dan terjadi setiap saat.

Jika yang kamu maksud:

int& getInt() {
    int i;
    return i;  // DON'T DO THIS.
}

Itu semua jenis kejahatan. Tumpukan yang dialokasikan iakan hilang dan Anda tidak merujuk apa pun. Ini juga jahat:

int& getInt() {
    int* i = new int;
    return *i;  // DON'T DO THIS.
}

Karena sekarang klien pada akhirnya harus melakukan yang aneh:

int& myInt = getInt(); // note the &, we cannot lose this reference!
delete &myInt;         // must delete...totally weird and  evil

int oops = getInt(); 
delete &oops; // undefined behavior, we're wrongly deleting a copy, not the original

Perhatikan bahwa referensi nilai masih hanya referensi, jadi semua aplikasi jahat tetap sama.

Jika Anda ingin mengalokasikan sesuatu yang hidup di luar cakupan fungsi, gunakan pointer pintar (atau secara umum, wadah):

std::unique_ptr<int> getInt() {
    return std::make_unique<int>(0);
}

Dan sekarang klien menyimpan pointer pintar:

std::unique_ptr<int> x = getInt();

Referensi juga boleh digunakan untuk mengakses hal-hal yang Anda tahu seumur hidup tetap terbuka pada tingkat yang lebih tinggi, misalnya:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

Di sini kita tahu tidak apa-apa untuk mengembalikan referensi i_karena apa pun yang memanggil kita mengelola masa instance kelas, jadi i_akan hidup setidaknya selama itu.

Dan tentu saja, tidak ada yang salah dengan hanya:

int getInt() {
   return 0;
}

Jika seumur hidup harus diserahkan kepada penelepon, dan Anda hanya menghitung nilainya.

Ringkasan: tidak apa-apa untuk mengembalikan referensi jika umur objek tidak akan berakhir setelah panggilan.

GManNickG
sumber
21
Ini semua adalah contoh buruk. Contoh terbaik penggunaan yang tepat adalah ketika Anda mengembalikan referensi ke objek yang disahkan.
Ala
171
Demi keturunan, dan untuk setiap programmer yang lebih baru yang memanfaatkan ini, petunjuknya tidak buruk . Baik pointer ke memori dinamis buruk. Keduanya memiliki tempat sah di C ++. Smart pointer seharusnya menjadi pilihan utama Anda ketika datang ke manajemen memori dinamis, tetapi pointer cerdas default Anda harus unique_ptr, bukan shared_ptr.
Jamin Gray
12
Edit menyetujui: jangan menyetujui pengeditan jika Anda tidak dapat menjamin kebenarannya. Saya telah mengembalikan suntingan yang salah.
GManNickG
7
Demi keturunan, dan untuk setiap programmer yang lebih baru yang membahas ini, jangan menulisreturn new int .
Lightness Races di Orbit
3
Demi keturunan, dan untuk setiap programmer yang lebih baru yang kebetulan tentang ini, kembalikan T dari fungsi. RVO akan mengurus semuanya.
Sepatu
64

Tidak. Tidak, tidak, seribu kali tidak.

Apa yang jahat adalah membuat referensi ke objek yang dialokasikan secara dinamis dan kehilangan pointer asli. Ketika Anda newsebuah objek Anda mengasumsikan kewajiban untuk memiliki jaminan delete.

Tetapi lihat, misalnya operator<<: yang harus mengembalikan referensi, atau

cout << "foo" << "bar" << "bletch" << endl ;

tidak akan bekerja

Charlie Martin
sumber
23
Saya kalah karena ini tidak menjawab pertanyaan (di mana OP telah menjelaskan dia tahu perlunya penghapusan) atau mengatasi ketakutan yang sah bahwa mengembalikan referensi ke objek freestore dapat menyebabkan kebingungan. Mendesah.
4
Praktek mengembalikan objek referensi bukanlah kejahatan. Ergo no. Ketakutan yang dia ungkapkan adalah ketakutan yang benar, seperti yang saya tunjukkan dalam graf kedua.
Charlie Martin
Anda sebenarnya tidak. Tapi ini tidak sepadan dengan waktuku.
2
Iraimbilanja @ Tentang "Tidak" -s saya tidak peduli. tetapi posting ini menunjukkan info penting yang hilang dari GMan.
Kobor42
48

Anda harus mengembalikan referensi ke objek yang ada yang tidak segera pergi, dan di mana Anda tidak bermaksud melakukan transfer kepemilikan.

Jangan pernah mengembalikan referensi ke variabel lokal atau semacamnya, karena itu tidak akan ada untuk dirujuk.

Anda dapat mengembalikan referensi ke sesuatu yang terpisah dari fungsi, yang tidak Anda harapkan fungsi pemanggilannya bertanggung jawab untuk dihapus. Ini adalah kasus untuk tipikaloperator[] fungsi .

Jika Anda membuat sesuatu, Anda harus mengembalikan nilai atau penunjuk (biasa atau pintar). Anda dapat mengembalikan nilai secara bebas, karena akan menjadi variabel atau ekspresi dalam fungsi panggilan. Jangan pernah mengembalikan pointer ke variabel lokal, karena itu akan hilang.

David Thornley
sumber
1
Jawaban yang sangat bagus tetapi untuk "Anda dapat mengembalikan sementara sebagai referensi const." Kode berikut akan dikompilasi tetapi mungkin macet karena sementara dihancurkan pada akhir pernyataan kembali: "int const & f () {return 42;} void main () {int const & r = f (); ++ r;} "
j_random_hacker
@j_random_hacker: C ++ memiliki beberapa aturan aneh untuk referensi ke temporaries, di mana masa pakai sementara mungkin diperpanjang. Maaf saya tidak memahaminya dengan cukup baik untuk mengetahui apakah itu mencakup kasus Anda.
Mark Ransom
3
@ Mark: Ya, memang ada beberapa aturan aneh. Seumur hidup sementara hanya dapat diperpanjang dengan menginisialisasi referensi const (yang bukan anggota kelas) dengan itu; kemudian hidup sampai wasit keluar dari ruang lingkup. Sayangnya, mengembalikan konstitusi tidak tercakup. Namun mengembalikan nilai temp adalah aman.
j_random_hacker
Lihat Standar C ++, 12.2, paragraf 5. Juga lihat Guru Tersesat Herb Sutter of the Week di herbutter.wordpress.com/2008/01/01/… .
David Thornley
4
@ David: Ketika tipe pengembalian fungsi adalah "T const &", yang sebenarnya terjadi adalah bahwa pernyataan return secara implisit mengubah temp, yang bertipe T, untuk mengetik "T const &" sesuai 6.6.3.2 (konversi legal tetapi satu yang tidak memperpanjang masa pakai), dan kemudian kode panggilan menginisialisasi ref dari tipe "T const &" dengan hasil fungsi, juga dari tipe "T const &" - ​​lagi-lagi, proses legal tetapi tidak memperpanjang seumur hidup. Hasil akhirnya: tidak ada perpanjangan seumur hidup, dan banyak kebingungan. :(
j_random_hacker
26

Saya menemukan jawabannya tidak memuaskan jadi saya akan menambahkan dua sen saya.

Mari kita menganalisis kasus-kasus berikut:

Penggunaan yang salah

int& getInt()
{
    int x = 4;
    return x;
}

Ini jelas kesalahan

int& x = getInt(); // will refer to garbage

Penggunaan dengan variabel statis

int& getInt()
{
   static int x = 4;
   return x;
}

Ini benar, karena variabel statis ada sepanjang masa program.

int& x = getInt(); // valid reference, x = 4

Ini juga cukup umum ketika menerapkan pola Singleton

Class Singleton
{
    public:
        static Singleton& instance()
        {
            static Singleton instance;
            return instance;
        };

        void printHello()
        {
             printf("Hello");
        };

}

Pemakaian:

 Singleton& my_sing = Singleton::instance(); // Valid Singleton instance
 my_sing.printHello();  // "Hello"

Operator

Kontainer perpustakaan standar sangat bergantung pada penggunaan operator yang mengembalikan referensi, misalnya

T & operator*();

dapat digunakan sebagai berikut

std::vector<int> x = {1, 2, 3}; // create vector with 3 elements
std::vector<int>::iterator iter = x.begin(); // iterator points to first element (1)
*iter = 2; // modify first element, x = {2, 2, 3} now

Akses cepat ke data internal

Ada saat-saat & dapat digunakan untuk akses cepat ke data internal

Class Container
{
    private:
        std::vector<int> m_data;

    public:
        std::vector<int>& data()
        {
             return m_data;
        }
}

dengan penggunaan:

Container cont;
cont.data().push_back(1); // appends element to std::vector<int>
cont.data()[0] // 1

NAMUN, ini dapat menyebabkan jebakan seperti ini:

Container* cont = new Container;
std::vector<int>& cont_data = cont->data();
cont_data.push_back(1);
delete cont; // This is bad, because we still have a dangling reference to its internal data!
cont_data[0]; // dangling reference!
thorhunter
sumber
Mengembalikan referensi ke variabel statis dapat menyebabkan perilaku yang tidak diinginkan, mis. Pertimbangkan operator gandakan yang mengembalikan referensi ke anggota statis maka yang berikut akan selalu menghasilkan true:If((a*b) == (c*d))
SebNag
Container::data()Implementasi harus membacareturn m_data;
Xeaz
Ini sangat membantu, terima kasih! @Xeaz bukankah itu akan menyebabkan masalah dengan panggilan penambahan?
Andrew
@ Sedu Mengapa kamu ingin melakukan itu?
thorhunter
@Andrew Tidak, itu adalah shenanigan sintaksis. Jika Anda, misalnya, mengembalikan tipe pointer, maka Anda akan menggunakan alamat referensi untuk membuat dan mengembalikan pointer.
thorhunter
14

Itu tidak jahat. Seperti banyak hal dalam C ++, ada baiknya jika digunakan dengan benar, tetapi ada banyak jebakan yang harus Anda perhatikan saat menggunakannya (seperti mengembalikan referensi ke variabel lokal).

Ada hal-hal baik yang dapat dicapai dengannya (seperti peta [nama] = "halo dunia")

Mehrdad Afshari
sumber
1
Saya hanya ingin tahu, apa gunanya map[name] = "hello world"?
nama pengguna salah
5
@wrongusername Sintaksnya intuitif. Pernah mencoba menambah hitungan nilai yang disimpan di HashMap<String,Integer>dalam Java? : P
Mehrdad Afshari
Haha, belum, tetapi melihat contoh HashMap, memang terlihat cukup degil: D
nama pengguna yang salah
Masalah yang saya miliki dengan ini: Fungsi mengembalikan referensi ke objek dalam wadah, tetapi kode fungsi panggilan menetapkannya ke variabel lokal. Kemudian memodifikasi beberapa properti objek. Masalah: Objek asli dalam wadah tidak tersentuh. Programmer begitu mudah mengabaikan nilai pengembalian &, dan kemudian Anda mendapatkan perilaku yang benar-benar tak terduga ...
flohack
10

"Mengembalikan referensi adalah kejahatan karena, hanya [seperti yang saya mengerti] itu membuatnya lebih mudah untuk tidak menghapusnya"

Tidak benar. Mengembalikan referensi tidak menyiratkan kepemilikan semantik. Yaitu, hanya karena Anda melakukan ini:

Value& v = thing->getTheValue();

... tidak berarti Anda sekarang memiliki memori yang disebut oleh v;

Namun, ini adalah kode yang mengerikan:

int& getTheValue()
{
   return *new int;
}

Jika Anda melakukan sesuatu seperti ini karena "Anda tidak memerlukan pointer pada contoh itu" maka: 1) cukup referensi pointer jika Anda memerlukan referensi, dan 2) Anda akhirnya akan membutuhkan pointer, karena Anda harus mencocokkan baru dengan delete, dan Anda perlu pointer untuk memanggil delete.

John Dibling
sumber
7

Ada dua kasus:

  • referensi const - ide bagus, kadang-kadang, terutama untuk objek berat atau kelas proxy, pengoptimalan kompiler

  • referensi non-const --bad ide, kadang-kadang, istirahat enkapsulasi

Keduanya berbagi masalah yang sama - berpotensi menunjuk ke objek yang hancur ...

Saya akan merekomendasikan menggunakan smart pointer untuk banyak situasi di mana Anda harus mengembalikan referensi / pointer.

Perhatikan juga hal-hal berikut:

Ada aturan formal - Standar C ++ (bagian 13.3.3.1.4 jika Anda tertarik) menyatakan bahwa sementara hanya dapat terikat ke referensi const - jika Anda mencoba menggunakan referensi non-const, kompiler harus menandai ini sebagai kesalahan.

Sasha
sumber
1
non-const ref tidak selalu merusak enkapsulasi. pertimbangkan vector :: operator []
itu kasus yang sangat istimewa ... itu sebabnya saya kadang-kadang berkata, meskipun saya harus benar-benar mengklaim PALING SAJA :)
Jadi, Anda mengatakan bahwa implementasi operator subskrip yang normal adalah kejahatan yang diperlukan? Saya tidak setuju atau setuju dengan ini; karena saya bukan orang yang lebih bijak.
Nick Bolton
Saya tidak mengatakan bahwa, tapi bisa jahat jika disalahgunakan :))) vektor :: di harus digunakan bila memungkinkan ....
eh? vector :: at juga mengembalikan ref nonconst.
4

Bukan saja itu bukan kejahatan, terkadang juga penting. Sebagai contoh, akan mustahil untuk mengimplementasikan operator [] std :: vector tanpa menggunakan nilai balik referensi.

segera
sumber
Ah, ya tentu saja; Saya pikir inilah mengapa saya mulai menggunakannya; seperti ketika saya pertama kali mengimplementasikan operator subskrip [] saya menyadari penggunaan referensi. Saya memimpin untuk percaya bahwa ini adalah de facto.
Nick Bolton
Anehnya, Anda dapat menerapkan operator[]untuk wadah tanpa menggunakan referensi ... dan std::vector<bool>tidak. (Dan menciptakan kekacauan nyata dalam prosesnya)
Ben Voigt
@ BenVoigt mmm, mengapa berantakan? Mengembalikan proxy juga merupakan skenario yang valid untuk kontainer dengan penyimpanan kompleks yang tidak memetakan langsung ke tipe luar (seperti ::std::vector<bool>yang telah Anda sebutkan).
Sergey.quixoticaxis.Ivanov
1
@ Sergey.quixoticaxis.Ivanov: Kekacauannya adalah bahwa menggunakan std::vector<T>kode templat rusak, jika Tmungkin bool, karena std::vector<bool>memiliki perilaku yang sangat berbeda dari instansi lain. Ini berguna, tetapi seharusnya diberi nama sendiri dan bukan spesialisasi std::vector.
Ben Voigt
@BenVoight Saya setuju pada poin tentang keputusan aneh membuat satu spesialisasi "benar-benar istimewa" tapi saya merasa bahwa komentar asli Anda menyiratkan bahwa mengembalikan proxy adalah aneh pada umumnya.
Sergey.quixoticaxis.Ivanov
2

Tambahan untuk jawaban yang diterima:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

Saya berpendapat bahwa contoh ini tidak baik dan harus dihindari jika memungkinkan. Mengapa? Sangat mudah untuk berakhir dengan referensi yang menggantung .

Untuk mengilustrasikan poin dengan contoh:

struct Foo
{
    Foo(int i = 42) : boo_(i) {}
    immutableint boo()
    {
        return boo_;
    }  
private:
    immutableint boo_;
};

memasuki zona bahaya:

Foo foo;
const int& dangling = foo.boo().get(); // dangling reference!
AMA
sumber
1

referensi pengembalian biasanya digunakan dalam overloading operator di C ++ untuk Objek besar, karena mengembalikan nilai perlu operasi penyalinan (dalam overloading perator, kami biasanya tidak menggunakan pointer sebagai nilai balik)

Tetapi pengembalian referensi dapat menyebabkan masalah alokasi memori. Karena referensi ke hasil akan dikeluarkan dari fungsi sebagai referensi ke nilai kembali, nilai kembali tidak bisa menjadi variabel otomatis.

jika Anda ingin menggunakan referensi yang kembali, Anda dapat menggunakan buffer objek statis. sebagai contoh

const max_tmp=5; 
Obj& get_tmp()
{
 static int buf=0;
 static Obj Buf[max_tmp];
  if(buf==max_tmp) buf=0;
  return Buf[buf++];
}
Obj& operator+(const Obj& o1, const Obj& o1)
{
 Obj& res=get_tmp();
 // +operation
  return res;
 }

dengan cara ini, Anda dapat menggunakan referensi pengembalian dengan aman.

Tapi Anda selalu bisa menggunakan pointer daripada referensi untuk mengembalikan nilai di functiong.

MinandLucy
sumber
0

saya pikir menggunakan referensi sebagai nilai kembali fungsi jauh lebih lurus daripada menggunakan pointer sebagai nilai balik fungsi. Kedua Akan selalu aman untuk menggunakan variabel statis yang merujuk nilai kembali.

Tony
sumber
0

Hal terbaik adalah membuat objek dan meneruskannya sebagai parameter referensi / penunjuk ke fungsi yang mengalokasikan variabel ini.

Mengalokasikan objek dalam fungsi dan mengembalikannya sebagai referensi atau pointer (pointer lebih aman namun) adalah ide yang buruk karena membebaskan memori di akhir blok fungsi.

Drezir
sumber
-1
    Class Set {
    int *ptr;
    int size;

    public: 
    Set(){
     size =0;
         }

     Set(int size) {
      this->size = size;
      ptr = new int [size];
     }

    int& getPtr(int i) {
     return ptr[i];  // bad practice 
     }
  };

Fungsi getPtr dapat mengakses memori dinamis setelah penghapusan atau bahkan objek null. Yang dapat menyebabkan Pengecualian Akses Buruk. Sebaliknya pengambil dan penyetel harus diimplementasikan dan ukuran diverifikasi sebelum kembali.

Amarbir
sumber
-2

Berfungsi sebagai lvalue (alias, pengembalian referensi non-const) harus dihapus dari C ++. Ini sangat tidak intuitif. Scott Meyers ingin min () dengan perilaku ini.

min(a,b) = 0;  // What???

yang tidak benar-benar perbaikan

setmin (a, b, 0);

Yang terakhir bahkan lebih masuk akal.

Saya menyadari bahwa fungsi sebagai lvalue penting untuk aliran gaya C ++, tetapi perlu menunjukkan bahwa aliran gaya C ++ mengerikan. Saya bukan satu-satunya yang berpikir ini ... seingat saya, Alexandrescu memiliki artikel besar tentang bagaimana melakukan yang lebih baik, dan saya percaya dorongan juga telah mencoba untuk menciptakan metode I / O aman jenis yang lebih baik.

Dan Olson
sumber
2
Tentu berbahaya, dan harus ada pengecekan kesalahan kompiler yang lebih baik, tetapi tanpa itu beberapa konstruksi yang berguna tidak dapat dilakukan, misalnya operator [] () di std :: map.
j_random_hacker
2
Mengembalikan referensi non-const sebenarnya sangat berguna. vector::operator[]sebagai contoh. Apakah Anda lebih suka menulis v.setAt(i, x)atau v[i] = x? Yang terakhir adalah JAUH superior.
Miles
1
@MilesRout Saya akan pergi v.setAt(i, x)kapan saja. Jauh lebih unggul.
scravy
-2

Saya mengalami masalah nyata di mana itu memang jahat. Pada dasarnya seorang pengembang mengembalikan referensi ke objek dalam vektor. Itu buruk!!!

Rincian lengkap yang saya tulis di Janurary: http://developer-resource.blogspot.com/2009/01/pros-and-cons-of-returing-references.html

mengembangkan sumber daya
sumber
2
Jika Anda perlu mengubah nilai asli dalam kode panggilan, maka Anda harus mengembalikan referensi. Dan itu sebenarnya tidak lebih dan tidak lebih berbahaya daripada mengembalikan iterator ke vektor - keduanya dibatalkan jika elemen ditambahkan atau dihapus dari vektor.
j_random_hacker
Masalah khusus itu disebabkan oleh memegang referensi ke elemen vektor, dan kemudian memodifikasi vektor itu dengan cara yang membatalkan referensi: Halaman 153, bagian 6.2 dari "C ++ Standard Library: A Tutorial and Reference" - Josuttis, membaca: "Memasukkan atau menghapus elemen membatalkan referensi, pointer, dan iterator yang merujuk pada elemen berikut. Jika sebuah insersi menyebabkan realokasi, itu membatalkan semua referensi, iterator, dan pointer "
Trent
-15

Tentang kode mengerikan:

int& getTheValue()
{
   return *new int;
}

Jadi, memang, penunjuk memori hilang setelah kembali. Tetapi jika Anda menggunakan shared_ptr seperti itu:

int& getTheValue()
{
   std::shared_ptr<int> p(new int);
   return *p->get();
}

Memori tidak hilang setelah kembali dan akan dibebaskan setelah penugasan.

Anatoly
sumber
12
Itu hilang karena pointer bersama keluar dari ruang lingkup dan membebaskan integer.
pointer tidak hilang, alamat referensi adalah pointer.
dgsomerton