Pertama kita perlu kembali ke apa artinya lulus dengan nilai dan referensi.
Untuk bahasa seperti Java dan SML, pass by value sangat mudah (dan tidak ada pass by reference), sama seperti menyalin nilai variabel, karena semua variabel hanyalah skalar dan telah dibangun di dalam semantic copy: mereka adalah apa yang dihitung sebagai aritmatika ketik C ++, atau "referensi" (pointer dengan nama dan sintaks yang berbeda).
Di C kami memiliki tipe skalar dan yang ditentukan pengguna:
- Skalar memiliki nilai numerik atau abstrak (pointer bukan angka, mereka memiliki nilai abstrak) yang disalin.
- Jenis agregat memiliki semua anggota yang mungkin diinisialisasi yang mungkin disalin:
- untuk jenis produk (array dan struktur): secara rekursif, semua anggota struktur dan elemen array disalin (sintaks fungsi C tidak memungkinkan untuk meneruskan array dengan nilai secara langsung, hanya susunan anggota struct, tetapi itu adalah detail ).
- untuk tipe penjumlahan (serikat pekerja): nilai "anggota aktif" dipertahankan; jelas, salinan anggota demi anggota tidak berurutan karena tidak semua anggota dapat diinisialisasi.
Dalam C ++ tipe yang ditentukan pengguna dapat memiliki semantik salin yang ditentukan pengguna, yang memungkinkan pemrograman "berorientasi objek" dengan objek dengan kepemilikan sumber daya dan operasi "salinan dalam". Dalam kasus seperti itu, operasi penyalinan sebenarnya adalah panggilan ke fungsi yang hampir dapat melakukan operasi sewenang-wenang.
Untuk C struct yang dikompilasi sebagai C ++, "penyalinan" masih didefinisikan sebagai memanggil operasi penyalinan yang ditentukan pengguna (baik operator konstruktor atau penugasan), yang secara implisit dihasilkan oleh kompiler. Ini berarti bahwa semantik program subset umum C / C ++ berbeda dalam C dan C ++: dalam C tipe agregat keseluruhan disalin, dalam C ++ fungsi penyalinan yang dihasilkan secara implisit dipanggil untuk menyalin setiap anggota; hasil akhirnya adalah bahwa dalam setiap kasus masing-masing anggota disalin.
(Saya pikir, ada pengecualian ketika struct di dalam sebuah serikat disalin.)
Jadi untuk tipe kelas, satu-satunya cara (di luar salinan union) untuk membuat instance baru adalah melalui konstruktor (bahkan bagi mereka yang memiliki konstruktor kompiler yang dihasilkan sepele).
Anda tidak dapat mengambil alamat nilai melalui operator unary &
tetapi itu tidak berarti bahwa tidak ada objek nilai; dan suatu objek, menurut definisi, memiliki alamat ; dan alamat itu bahkan diwakili oleh konstruksi sintaks: objek tipe kelas hanya dapat dibuat oleh konstruktor, dan memiliki this
pointer; tetapi untuk jenis sepele, tidak ada konstruktor yang ditulis pengguna sehingga tidak ada tempat untuk meletakkan this
sampai setelah salinan dibuat, dan dinamai.
Untuk jenis skalar, nilai suatu objek adalah nilai dari objek, nilai matematika murni yang disimpan ke dalam objek.
Untuk jenis kelas, satu-satunya gagasan tentang nilai objek adalah salinan lain dari objek, yang hanya dapat dibuat oleh copy constructor, fungsi nyata (walaupun untuk tipe sepele yang fungsinya sangat khusus sepele, kadang-kadang ini bisa menjadi dibuat tanpa memanggil konstruktor). Itu berarti bahwa nilai objek adalah hasil dari perubahan status program global dengan eksekusi . Itu tidak mengakses secara matematis.
Jadi, lulus dengan nilai sebenarnya bukan hal: itu lewat panggilan copy constructor , yang kurang cantik. Copy constructor diharapkan untuk melakukan operasi "copy" yang masuk akal sesuai dengan semantik yang tepat dari tipe objek, dengan menghormati invarian internalnya (yang merupakan properti pengguna abstrak, bukan properti C ++ intrinsik).
Lewati dengan nilai objek kelas berarti:
- buat contoh lain
- kemudian buat fungsi dipanggil bertindak pada contoh itu.
Perhatikan bahwa masalah tidak ada hubungannya dengan apakah salinan itu sendiri adalah objek dengan alamat: semua parameter fungsi adalah objek dan memiliki alamat (pada tingkat semantik bahasa).
Masalahnya adalah apakah:
- salinan adalah objek baru yang diinisialisasi dengan nilai matematika murni (true pure rvalue) dari objek asli, seperti halnya skalar;
- atau salinannya adalah nilai objek asli , seperti pada kelas.
Dalam kasus tipe kelas sepele, Anda masih dapat menentukan anggota salinan anggota asli, sehingga Anda bisa menentukan nilai murni asli karena sepele dari operasi penyalinan (copy constructor dan assignment). Tidak demikian halnya dengan fungsi pengguna khusus yang sewenang-wenang: nilai dokumen asli harus berupa salinan yang dibuat.
Objek kelas harus dibangun oleh penelepon; konstruktor secara formal memiliki this
pointer tetapi formalisme tidak relevan di sini: semua objek secara formal memiliki alamat tetapi hanya mereka yang benar-benar mendapatkan alamatnya digunakan dengan cara yang tidak murni lokal (tidak seperti *&i = 1;
yang menggunakan alamat lokal murni) perlu memiliki definisi yang baik alamat.
Objek harus benar-benar melalui alamat jika harus memiliki alamat di kedua fungsi yang dikompilasi secara terpisah ini:
void callee(int &i) {
something(&i);
}
void caller() {
int i;
callee(i);
something(&i);
}
Di sini bahkan jika something(address)
adalah fungsi murni atau makro atau apa pun (seperti printf("%p",arg)
) yang tidak dapat menyimpan alamat atau berkomunikasi dengan entitas lain, kami memiliki persyaratan untuk melewati alamat karena alamat harus didefinisikan dengan baik untuk objek unik int
yang memiliki keunikan identitas.
Kami tidak tahu apakah fungsi eksternal akan "murni" dalam hal alamat yang diteruskan ke sana.
Di sini potensi untuk penggunaan nyata dari alamat baik dalam konstruktor non sepele atau destruktor di sisi pemanggil mungkin alasan untuk mengambil rute yang aman dan sederhana dan memberikan objek identitas di pemanggil dan menyampaikan alamatnya, karena itu membuat yakin bahwa setiap penggunaan non-sepele alamatnya di konstruktor, setelah konstruksi dan di destruktor konsisten : this
harus tampak sama atas keberadaan objek.
Konstruktor atau destruktor non trivial seperti fungsi lain dapat menggunakan this
pointer dengan cara yang membutuhkan konsistensi atas nilainya meskipun beberapa objek dengan hal-hal non sepele mungkin tidak:
struct file_handler { // don't use that class!
file_handler () { this->fileno = -1; }
file_handler (int f) { this->fileno = f; }
file_handler (const file_handler& rhs) {
if (this->fileno != -1)
this->fileno = dup(rhs.fileno);
else
this->fileno = -1;
}
~file_handler () {
if (this->fileno != -1)
close(this->fileno);
}
file_handler &operator= (const file_handler& rhs);
};
Perhatikan bahwa dalam kasus itu, meskipun menggunakan pointer secara eksplisit (sintaksis eksplisit this->
), identitas objek tidak relevan: kompiler dapat menggunakan bitwise untuk menyalin objek sekitar untuk memindahkannya dan melakukan "salin elisi". Ini didasarkan pada tingkat "kemurnian" penggunaan this
dalam fungsi anggota khusus (alamat tidak lepas).
Tetapi kemurnian bukanlah atribut yang tersedia di tingkat deklarasi standar (ada ekstensi kompiler yang menambahkan deskripsi kemurnian pada deklarasi fungsi non-inline), jadi Anda tidak dapat mendefinisikan ABI berdasarkan pada kemurnian kode yang mungkin tidak tersedia (kode mungkin atau mungkin tidak sebaris dan tersedia untuk analisis).
Kemurnian diukur sebagai "tentu murni" atau "tidak murni atau tidak diketahui". Dasar bersama, atau batas atas semantik (sebenarnya maksimum), atau LCM (Least Common Multiple) "tidak diketahui". Jadi ABI memutuskan tidak diketahui.
Ringkasan:
- Beberapa konstruk memerlukan kompiler untuk mendefinisikan identitas objek.
- ABI didefinisikan dalam istilah kelas program dan bukan kasus khusus yang mungkin dioptimalkan.
Kemungkinan pekerjaan di masa depan:
Apakah penjelasan kemurnian cukup bermanfaat untuk digeneralisasi dan distandarisasi?