Bagaimana saya bisa mendapatkan alamat objek dengan andal ketika operator & kelebihan beban?

170

Pertimbangkan program berikut:

struct ghost
{
    // ghosts like to pretend that they don't exist
    ghost* operator&() const volatile { return 0; }
};

int main()
{
    ghost clyde;
    ghost* clydes_address = &clyde; // darn; that's not clyde's address :'( 
}

Bagaimana cara saya mendapatkan clydealamat?

Saya mencari solusi yang akan bekerja sama baiknya untuk semua jenis objek. Solusi C ++ 03 akan lebih baik, tetapi saya juga tertarik pada solusi C ++ 11. Jika memungkinkan, mari kita hindari perilaku spesifik implementasi.

Saya mengetahui std::addressoftemplat fungsi C ++ 11 , tetapi saya tidak tertarik menggunakannya di sini: Saya ingin memahami bagaimana implementor Perpustakaan Standar dapat mengimplementasikan templat fungsi ini.

James McNellis
sumber
41
@jalf: Strategi itu dapat diterima, tetapi sekarang saya telah memukul orang-orang tersebut di kepala, bagaimana saya menangani kode mereka yang keji? :-)
James McNellis
5
@jalf Uhm, kadang-kadang Anda perlu membebani operator ini, dan mengembalikan objek proxy. Meskipun aku tidak bisa memikirkan contoh barusan.
Konrad Rudolph
5
@Konrad: saya juga. Jika Anda membutuhkannya, saya akan menyarankan bahwa pilihan yang lebih baik mungkin untuk memikirkan kembali desain Anda, karena membebani operator yang hanya menyebabkan terlalu banyak masalah. :)
jalf
2
@ Konrad: Dalam sekitar 20 tahun pemrograman C ++ saya pernah mencoba untuk membebani operator itu. Itu adalah awal dari dua puluh tahun itu. Oh, dan saya gagal membuat itu bisa digunakan. Akibatnya, entri FAQ operator yang kelebihan beban mengatakan, "Alamat operator yang tidak waspada tidak boleh kelebihan beban." Anda akan mendapatkan bir gratis saat kami bertemu berikutnya jika Anda dapat memberikan contoh yang meyakinkan untuk membebani operator ini secara berlebihan. (Saya tahu Anda akan meninggalkan Berlin, jadi saya bisa menawarkan ini dengan aman :))
sbi
5
CComPtr<>dan CComQIPtr<>kelebihan bebanoperator&
Simon Richter

Jawaban:

102

Pembaruan: di C ++ 11, orang dapat menggunakan std::addressofsebagai ganti boost::addressof.


Pertama-tama mari kita salin kode dari Boost, minus pekerjaan kompiler di sekitar bit:

template<class T>
struct addr_impl_ref
{
  T & v_;

  inline addr_impl_ref( T & v ): v_( v ) {}
  inline operator T& () const { return v_; }

private:
  addr_impl_ref & operator=(const addr_impl_ref &);
};

template<class T>
struct addressof_impl
{
  static inline T * f( T & v, long ) {
    return reinterpret_cast<T*>(
        &const_cast<char&>(reinterpret_cast<const volatile char &>(v)));
  }

  static inline T * f( T * v, int ) { return v; }
};

template<class T>
T * addressof( T & v ) {
  return addressof_impl<T>::f( addr_impl_ref<T>( v ), 0 );
}

Apa yang terjadi jika kita meneruskan referensi ke fungsi ?

Catatan: addressoftidak dapat digunakan dengan pointer berfungsi

Dalam C ++ jika void func();dideklarasikan, maka funcadalah referensi ke fungsi tanpa argumen dan tidak mengembalikan hasil. Referensi ke fungsi ini dapat secara sepele dikonversi menjadi pointer ke fungsi - dari @Konstantin: Menurut 13.3.3.2 keduanya T &dan T *tidak dapat dibedakan untuk fungsi. Yang pertama adalah konversi Identity dan yang kedua adalah konversi Function-to-Pointer yang keduanya memiliki peringkat "Pencocokan Tepat" (13.3.3.1.1 tabel 9).

The referensi ke fungsi melewati addr_impl_ref, ada ambiguitas dalam resolusi yang berlebihan untuk pilihan f, yang diselesaikan berkat argumen boneka 0, yang merupakan intpertama dan bisa dipromosikan ke long(Konversi Integral).

Jadi kita cukup mengembalikan pointer.

Apa yang terjadi jika kami melewati jenis dengan operator konversi?

Jika operator konversi menghasilkan a T*maka kami memiliki ambiguitas: untuk f(T&,long)Promosi Integral diperlukan untuk argumen kedua sedangkan untuk f(T*,int)operator konversi dipanggil pada yang pertama (terima kasih kepada @litb)

Saat itulah addr_impl_refmasuk. Standar C ++ mengamanatkan bahwa urutan konversi dapat berisi paling banyak satu konversi yang ditentukan pengguna. Dengan memasukkan jenis addr_impl_refdan memaksa penggunaan urutan konversi, kami "menonaktifkan" semua operator konversi yang disertakan.

Dengan demikian f(T&,long)kelebihan dipilih (dan Promosi Integral dilakukan).

Apa yang terjadi untuk jenis lainnya?

Dengan demikian f(T&,long)kelebihan dipilih, karena jenisnya tidak sesuai dengan T*parameter.

Catatan: dari keterangan dalam file tentang kompatibilitas Borland, array tidak membusuk ke pointer, tetapi diteruskan dengan referensi.

Apa yang terjadi pada kelebihan ini?

Kami ingin menghindari penerapan operator&ke jenis, karena mungkin telah kelebihan beban.

Standar menjamin yang reinterpret_castdapat digunakan untuk pekerjaan ini (lihat jawaban @Matteo Italia: 5.2.10 / 10).

Boost menambahkan beberapa fitur constdan volatilekualifikasi untuk menghindari peringatan penyusun (dan menggunakan a const_castuntuk menghapusnya dengan benar).

  • Diputar T&kechar const volatile&
  • Strip constdanvolatile
  • Terapkan &operator untuk mengambil alamatnya
  • Kembali ke a T*

The const/ volatilejuggling adalah sedikit ilmu hitam, tetapi tidak menyederhanakan pekerjaan (daripada memberikan 4 overload). Perhatikan bahwa sejak Tadalah wajar tanpa pengecualian, jika kita lulus ghost const&, maka T*adalah ghost const*, dengan demikian kualifikasi belum benar-benar hilang.

EDIT: pointer overload digunakan untuk fungsi pointer, saya sedikit mengubah penjelasan di atas. Saya masih tidak mengerti mengapa itu perlu .

Output ideone berikut merangkum ini, agak.

Matthieu M.
sumber
2
"Apa yang terjadi jika kita memberikan pointer?" bagian yang salah. Jika kita melewatkan sebuah pointer ke beberapa tipe U, alamat dari fungsi, tipe 'T' disimpulkan sebagai 'U *' dan addr_impl_ref akan memiliki dua kelebihan: 'f (U * &, long)' dan 'f (U **, int) ', jelas yang pertama akan dipilih.
Konstantin Oznobihin
@Konstantin: benar, saya berpikir bahwa dua foverloads di mana templat fungsi, sedangkan mereka adalah fungsi anggota biasa dari kelas templat, terima kasih telah menunjukkannya. (Sekarang saya hanya perlu mencari tahu apa gunanya kelebihan, tip?)
Matthieu M.
Ini adalah jawaban yang bagus dan dijelaskan dengan baik. Saya pikir ada sedikit lebih dari ini hanya "dilemparkan char*." Terima kasih, Matthieu.
James McNellis
@ James: Saya mendapat banyak bantuan dari @Konstantin yang akan memukul kepala saya dengan tongkat kapan pun saya melakukan kesalahan: D
Matthieu M.
3
Mengapa perlu mengatasi tipe yang memiliki fungsi konversi? Tidakkah lebih suka pencocokan tepat daripada menjalankan fungsi konversi apa pun T*? EDIT: Sekarang saya mengerti. Memang, tapi dengan 0argumen itu akan berakhir di salib-silang , jadi akan ambigu.
Johannes Schaub - litb
99

Gunakan std::addressof.

Anda dapat menganggapnya sebagai berikut di belakang layar:

  1. Menafsirkan kembali objek sebagai referensi-ke-char
  2. Ambil alamat itu (tidak akan menyebut kelebihan beban)
  3. Letakkan pointer kembali ke pointer tipe Anda.

Implementasi yang ada (termasuk Boost.Addressof) melakukan hal itu, hanya menjaga constdan volatilekualifikasi tambahan.

Konrad Rudolph
sumber
16
Saya suka penjelasan ini lebih baik daripada yang dipilih karena dapat mudah dipahami.
Kereta luncur
49

Trik di belakang boost::addressofdan implementasi yang disediakan oleh @Luc Danton bergantung pada keajaiban reinterpret_cast; standar secara eksplisit menyatakan pada §5.2.10 ¶10 itu

Ekspresi nilai lebih dari tipe T1dapat dilemparkan ke tipe "referensi ke T2" jika ekspresi tipe "pointer ke T1" dapat secara eksplisit dikonversi ke tipe "pointer ke T2" menggunakan a reinterpret_cast. Artinya, pemeran referensi reinterpret_cast<T&>(x)memiliki efek yang sama dengan konversi *reinterpret_cast<T*>(&x)dengan built-in &dan *operator. Hasilnya adalah nilai yang merujuk ke objek yang sama dengan nilai sumber, tetapi dengan jenis yang berbeda.

Sekarang, ini memungkinkan kita untuk mengkonversi referensi objek arbitrer ke a char &(dengan kualifikasi cv jika referensi tersebut memenuhi syarat cv), karena setiap pointer dapat dikonversi menjadi (mungkin memenuhi syarat cv) char *. Sekarang kita memiliki char &, operator kelebihan pada objek tidak lagi relevan, dan kita dapat memperoleh alamat dengan &operator builtin .

Implementasi boost menambahkan beberapa langkah untuk bekerja dengan objek yang memenuhi syarat cv: yang pertama reinterpret_castdilakukan const volatile char &, jika char &gips polos tidak akan bekerja untuk constdan / atau volatilereferensi ( reinterpret_casttidak dapat menghapus const). Kemudian constdan volatiledihapus dengan const_cast, alamat diambil dengan &, dan final reinterpet_castuntuk tipe "benar" dilakukan.

The const_castdiperlukan untuk menghapus const/ volatileyang bisa telah ditambahkan ke non-const / referensi volatile, tapi tidak "bahaya" apa adalah const/ volatilereferensi di tempat pertama, karena final reinterpret_castakan kembali add-cv-kualifikasi apakah itu ada di tempat pertama ( reinterpret_casttidak bisa menghapus consttetapi bisa menambahkannya).

Adapun sisa kode dalam addressof.hpp, tampaknya sebagian besar adalah untuk penyelesaian masalah. The static inline T * f( T * v, int )tampaknya diperlukan hanya untuk compiler Borland, namun kehadirannya memperkenalkan kebutuhan addr_impl_ref, jika jenis pointer akan ditangkap oleh kelebihan kedua ini.

Sunting : berbagai kelebihan memiliki fungsi yang berbeda, lihat @Matthieu M. jawaban yang sangat baik .

Yah, saya tidak lagi yakin akan hal ini juga; Saya harus menyelidiki lebih lanjut kode itu, tapi sekarang saya sedang memasak makan malam :), saya akan melihatnya nanti.

Matteo Italia
sumber
Matthieu M. penjelasan tentang melewati pointer ke addressof tidak benar. Jangan merusak jawaban Anda yang luar biasa dengan pengeditan seperti ini :)
Konstantin Oznobihin
"good appetit", penyelidikan lebih lanjut menunjukkan bahwa kelebihan tersebut dipanggil untuk referensi ke fungsi void func(); boost::addressof(func);. Namun menghapus kelebihan tidak mencegah gcc 4.3.4 dari mengkompilasi kode dan menghasilkan output yang sama, jadi saya masih tidak mengerti mengapa perlu untuk memiliki kelebihan ini.
Matthieu M.
@Matthieu: Sepertinya ada bug di gcc. Menurut 13.3.3.2 baik T & dan T * tidak dapat dibedakan untuk fungsi. Yang pertama adalah konversi Identity dan yang kedua adalah konversi Function-to-Pointer yang keduanya memiliki peringkat "Pencocokan Tepat" (13.3.3.1.1 tabel 9). Jadi perlu ada argumen tambahan.
Konstantin Oznobihin
@Matthieu: Baru mencobanya dengan gcc 4.3.4 ( ideone.com/2f34P ) dan mendapatkan ambiguitas seperti yang diharapkan. Apakah Anda mencoba fungsi anggota yang kelebihan beban seperti pada alamat implementasi atau templat fungsi gratis? Yang terakhir (seperti ideone.com/vjCRs ) akan menghasilkan kelebihan 'T *' karena dipilih untuk mengurangi aturan argumen pengurangan (14.8.2.1/2).
Konstantin Oznobihin
2
@curiousguy: Mengapa Anda pikir itu harus? Saya telah mereferensikan bagian standar khusus C ++ yang menentukan apa yang harus dilakukan oleh kompiler dan semua kompiler yang dapat saya akses (termasuk tetapi tidak terbatas pada gcc 4.3.4, Comeau-online, VC6.0-VC2010) melaporkan ambiguitas seperti yang telah saya jelaskan. Bisakah Anda jelaskan alasan Anda tentang kasus ini?
Konstantin Oznobihin
11

Saya telah melihat implementasi dari addressofmelakukan ini:

char* start = &reinterpret_cast<char&>(clyde);
ghost* pointer_to_clyde = reinterpret_cast<ghost*>(start);

Jangan tanya saya bagaimana menyesuaikannya!

Luc Danton
sumber
5
Hukum. char*adalah pengecualian yang tercantum untuk mengetikkan aturan aliasing.
Anak Anjing
6
@DeadMG Saya tidak mengatakan ini tidak sesuai. Saya mengatakan bahwa Anda seharusnya tidak bertanya kepada saya :)
Luc Danton
1
@DeadMG Tidak ada masalah alias di sini. Pertanyaannya adalah: reinterpret_cast<char*>didefinisikan dengan baik.
curiousguy
2
@curiousguy dan jawabannya adalah ya, selalu diizinkan untuk melemparkan semua jenis pointer ke [unsigned] char *dan dengan demikian membaca representasi objek dari objek yang ditunjuk. Ini adalah area lain di mana charmemiliki hak istimewa khusus.
underscore_d
@underscore_d Hanya karena para pemain "selalu diizinkan" tidak berarti Anda dapat melakukan apa saja dengan hasil para pemain.
curiousguy
5

Lihatlah boost :: addressof dan implementasinya.

Konstantin Oznobihin
sumber
1
Kode Boost, meskipun menarik, tidak menjelaskan bagaimana tekniknya bekerja (juga tidak menjelaskan mengapa diperlukan dua kelebihan).
James McNellis
maksud Anda 'inline statis T * f (T * v, int)' berlebihan? Sepertinya itu diperlukan untuk penyelesaian Borland C saja. Pendekatan yang digunakan di sana cukup mudah. Satu-satunya hal yang halus (tidak standar) ada konversi 'T &' ke 'char &'. Meskipun standar, memungkinkan pemain dari 'T *' ke 'char *' tampaknya tidak ada persyaratan untuk pengecoran referensi. Namun demikian, orang mungkin berharap itu bekerja persis sama pada kebanyakan kompiler.
Konstantin Oznobihin
@Konstantin: kelebihan digunakan karena untuk sebuah pointer, addressofmengembalikan pointer itu sendiri. Dapat diperdebatkan apakah itu yang diinginkan atau tidak oleh pengguna, tetapi bagaimana cara yang ditentukan.
Matthieu M.
@Matthieu: Anda yakin? Sejauh yang saya tahu, tipe apa pun (termasuk tipe pointer) dibungkus di dalam addr_impl_ref, jadi pointer yang berlebihan tidak boleh disebut ...
Matteo Italia
1
@KonstantinOznobihin ini tidak benar-benar menjawab pertanyaan, karena semua yang Anda katakan adalah ke mana harus mencari jawabannya, bukan apa jawabannya .