Kode berikut terlihat agak tidak berbahaya pada pandangan pertama. Seorang pengguna menggunakan fungsi bar()
untuk berinteraksi dengan beberapa fungsi perpustakaan. (Ini mungkin telah bekerja untuk waktu yang lama sejak bar()
mengembalikan referensi ke nilai non-temporer atau serupa.) Namun sekarang hanya mengembalikan contoh baru B
. B
lagi memiliki fungsi a()
yang mengembalikan referensi ke objek dengan tipe iterateable A
. Pengguna ingin menanyakan objek ini yang mengarah ke segfault karena B
objek sementara yang dikembalikan oleh bar()
dihancurkan sebelum iterasi dimulai.
Saya ragu siapa (perpustakaan atau pengguna) yang harus disalahkan untuk ini. Semua kelas yang disediakan perpustakaan terlihat bersih bagi saya dan tentu saja tidak melakukan hal yang berbeda (mengembalikan referensi ke anggota, mengembalikan instance stack, ...) daripada begitu banyak kode lain di luar sana. Pengguna tampaknya tidak melakukan sesuatu yang salah juga, ia hanya mengulangi beberapa objek tanpa melakukan apa-apa mengenai objek itu seumur hidup.
(Pertanyaan terkait mungkin: Haruskah seseorang menetapkan aturan umum bahwa kode tidak boleh "rentang-berdasarkan-untuk-iterate" atas sesuatu yang diambil oleh lebih dari satu panggilan berantai di header loop karena salah satu dari panggilan ini dapat mengembalikan menilai kembali?)
#include <algorithm>
#include <iostream>
// "Library code"
struct A
{
A():
v{0,1,2}
{
std::cout << "A()" << std::endl;
}
~A()
{
std::cout << "~A()" << std::endl;
}
int * begin()
{
return &v[0];
}
int * end()
{
return &v[3];
}
int v[3];
};
struct B
{
A m_a;
A & a()
{
return m_a;
}
};
B bar()
{
return B();
}
// User code
int main()
{
for( auto i : bar().a() )
{
std::cout << i << std::endl;
}
}
Jawaban:
Saya pikir masalah mendasar adalah kombinasi fitur bahasa (atau ketiadaan) dari C ++. Baik kode perpustakaan dan kode klien masuk akal (sebagaimana dibuktikan oleh fakta bahwa masalahnya jauh dari jelas). Jika masa pakai sementara
B
itu sesuai diperpanjang (sampai akhir loop) tidak akan ada masalah.Membuat hidup sementara cukup lama, dan tidak lagi, sangat sulit. Bahkan tidak ad-hoc "semua temporaries yang terlibat dalam pembuatan kisaran untuk berbasis rentang untuk hidup sampai akhir loop" akan tanpa efek samping. Pertimbangkan kasus
B::a()
mengembalikan rentang yang tidak tergantung padaB
objek dengan nilai. Maka sementaraB
bisa segera dibuang. Bahkan jika seseorang dapat secara tepat mengidentifikasi kasus-kasus di mana perpanjangan seumur hidup diperlukan, karena kasus-kasus ini tidak jelas bagi programmer, efeknya (destruktor disebut jauh kemudian) akan mengejutkan dan mungkin sumber bug yang sama halusnya.Akan lebih baik jika hanya mendeteksi dan melarang omong kosong seperti itu, memaksa programmer untuk secara eksplisit meningkat
bar()
ke variabel lokal. Ini tidak mungkin di C ++ 11, dan mungkin tidak akan pernah mungkin karena membutuhkan anotasi. Rust melakukan ini, di mana tanda tangannya.a()
adalah:Berikut
'x
adalah variabel atau wilayah seumur hidup, yang merupakan nama simbolis untuk periode waktu sumber daya tersedia. Terus terang, masa hidup sulit untuk dijelaskan - atau kami belum menemukan penjelasan terbaik - jadi saya akan membatasi diri pada minimum yang diperlukan untuk contoh ini dan merujuk pembaca yang cenderung ke dokumentasi resmi .Pemeriksa pinjaman akan melihat bahwa hasil dari
bar().a()
kebutuhan untuk hidup selama loop berjalan. Diutarakan sebagai kendala pada seumur hidup'x
, kita menulis:'loop <= 'x
. Ini juga akan memperhatikan bahwa penerima panggilan metodebar()
,, bersifat sementara. Karena itu, kedua petunjuk tersebut dikaitkan dengan masa hidup yang sama'x <= 'temp
merupakan kendala lain.Dua kendala ini saling bertentangan! Kami membutuhkan
'loop <= 'x <= 'temp
tetapi'temp <= 'loop
, yang menangkap masalah dengan tepat. Karena persyaratan yang bertentangan, kode kereta ditolak. Perhatikan bahwa ini adalah pemeriksaan waktu kompilasi dan kode Rust biasanya menghasilkan kode mesin yang sama dengan kode C ++ yang setara, jadi Anda tidak perlu membayar biaya waktu untuk menjalankannya.Namun demikian ini adalah fitur besar untuk ditambahkan ke bahasa, dan hanya berfungsi jika semua kode menggunakannya. desain API juga terpengaruh (beberapa desain yang akan terlalu berbahaya di C ++ menjadi praktis, yang lain tidak dapat dibuat bermain bagus dengan masa hidup). Sayangnya, itu berarti tidak praktis untuk menambahkan C ++ (atau bahasa apa pun sebenarnya) secara surut. Singkatnya, kesalahannya adalah pada bahasa inersia yang berhasil dimiliki dan fakta bahwa Bjarne pada tahun 1983 tidak memiliki bola kristal dan pandangan jauh ke depan untuk memasukkan pelajaran dari 30 tahun terakhir penelitian dan pengalaman C ++ ;-)
Tentu saja, itu sama sekali tidak membantu dalam menghindari masalah di masa depan (kecuali jika Anda beralih ke Rust dan tidak pernah menggunakan C ++ lagi). Seseorang dapat menghindari ekspresi yang lebih lama dengan beberapa pemanggilan metode berantai (yang cukup membatasi, dan bahkan tidak memperbaiki semua masalah seumur hidup). Atau seseorang dapat mencoba mengadopsi kebijakan kepemilikan yang lebih disiplin tanpa bantuan penyusun: Dokumentasikan dengan jelas bahwa
bar
pengembalian berdasarkan nilai dan bahwa hasilB::a()
tidak boleh hidup lebih lamaB
dari yanga()
diminta. Ketika mengubah fungsi untuk kembali berdasarkan nilai alih-alih referensi yang berumur panjang, sadarilah bahwa ini adalah perubahan kontrak . Masih rawan kesalahan, tetapi dapat mempercepat proses mengidentifikasi penyebab ketika hal itu terjadi.sumber
Bisakah kita mengatasi masalah ini menggunakan fitur C ++?
C ++ 11 telah menambahkan kualifikasi fungsi anggota, yang memungkinkan membatasi kategori nilai instance kelas (ekspresi) fungsi anggota dapat dipanggil. Sebagai contoh:
Saat memanggil
begin
fungsi anggota, kita tahu bahwa kemungkinan besar kita juga perlu memanggilend
fungsi anggota (atau sesuatu sepertisize
, untuk mendapatkan ukuran kisaran). Ini mengharuskan kami beroperasi dengan nilai tinggi, karena kami harus mengatasinya dua kali. Karena itu Anda dapat berpendapat bahwa fungsi-fungsi anggota ini harus bernilai-ref-kualifikasi.Namun, ini mungkin tidak menyelesaikan masalah mendasar: aliasing. Fungsi
begin
danend
anggota alias objek, atau sumber daya yang dikelola oleh objek. Jika kita menggantibegin
danend
dengan satu fungsirange
, kita harus menyediakan satu yang dapat dipanggil pada nilai:Ini mungkin kasus penggunaan yang valid, tetapi definisi di atas
range
melarangnya. Karena kami tidak dapat mengatasi sementara setelah panggilan fungsi anggota, mungkin lebih masuk akal untuk mengembalikan sebuah wadah, yaitu rentang kepemilikan:Menerapkan ini ke kasus OP, dan sedikit ulasan kode
Fungsi anggota ini mengubah kategori nilai dari ekspresi:
B()
adalah nilai awal, tetapiB().a()
nilai rendah. Di sisi lain,B().m_a
adalah nilai. Jadi mari kita mulai dengan membuat ini konsisten. Ada dua cara untuk melakukan ini:Versi kedua, seperti yang disebutkan di atas, akan memperbaiki masalah di OP.
Selain itu, kami dapat membatasi
B
fungsi anggota:Ini tidak akan berdampak pada kode OP, karena hasil dari ekspresi setelah
:
dalam rentang berbasis untuk loop terikat ke variabel referensi. Dan variabel ini (sebagai ekspresi yang digunakan untuk mengakses fungsi anggotabegin
danend
) adalah nilai.Tentu saja, pertanyaannya adalah apakah aturan default harus "alias fungsi anggota pada nilai harus mengembalikan objek yang memiliki semua sumber dayanya, kecuali ada alasan bagus untuk tidak" . Alias yang dikembalikannya dapat digunakan secara legal, tetapi berbahaya seperti yang Anda alami: alias tidak dapat memperpanjang masa hidup "induk" sementara:
Di C ++ 2a, saya pikir Anda harus mengatasi masalah ini (atau yang serupa) sebagai berikut:
bukannya OP
Solusinya secara manual menentukan bahwa masa hidup
b
adalah seluruh blok for-loop.Proposal yang memperkenalkan init-statement ini
Demo Langsung
sumber