Kapan menjalankan fungsi anggota pada instance null menghasilkan perilaku yang tidak ditentukan?

120

Perhatikan kode berikut:

#include <iostream>

struct foo
{
    // (a):
    void bar() { std::cout << "gman was here" << std::endl; }

    // (b):
    void baz() { x = 5; }

    int x;
};

int main()
{
    foo* f = 0;

    f->bar(); // (a)
    f->baz(); // (b)
}

Kami berharap (b)untuk crash, karena tidak ada anggota yang sesuai xuntuk penunjuk null. Dalam praktiknya, (a)tidak macet karena thispenunjuk tidak pernah digunakan.

Karena (b)dereferensi thispointer ( (*this).x = 5;), dan thisnull, program memasuki perilaku tidak terdefinisi, karena dereferensi null selalu dikatakan sebagai perilaku tidak terdefinisi.

Apakah (a)menghasilkan perilaku yang tidak terdefinisi? Bagaimana jika kedua fungsi (dan x) statis?

GManNickG
sumber
Jika kedua fungsi tersebut statis , bagaimana x dapat dirujuk di dalam baz ? (x adalah variabel anggota non-statis)
legends2k
4
@ legends2k: Pura-pura xdibuat statis juga. :)
GManNickG
Tentunya, tetapi untuk kasus (a) ini bekerja sama dalam semua kasus, yaitu, fungsi tersebut dipanggil. Namun, mengganti nilai pointer dari 0 ke 1 (katakanlah, melalui reinterpret_cast), hampir selalu crash. Apakah alokasi nilai 0 dan dengan demikian NULL, seperti dalam kasus a, mewakili sesuatu yang khusus untuk kompilator? Mengapa selalu macet dengan nilai lain yang dialokasikan untuknya?
Siddharth Shankaran
5
Menarik: Ayo revisi C ++ berikutnya, tidak akan ada lagi dereferencing petunjuk sama sekali. Sekarang kita akan melakukan tipuan melalui pointer. Untuk mengetahui lebih lanjut, silakan lakukan tipuan melalui tautan ini: N3362
James McNellis
3
Memanggil fungsi anggota pada penunjuk null selalu merupakan perilaku yang tidak terdefinisi. Hanya dengan melihat kode Anda, saya sudah bisa merasakan perilaku tidak terdefinisi perlahan merangkak ke atas leher saya!
fredoverflow

Jawaban:

113

Keduanya (a)dan (b)menghasilkan perilaku yang tidak terdefinisi. Itu selalu merupakan perilaku yang tidak terdefinisi untuk memanggil fungsi anggota melalui penunjuk null. Jika fungsinya statis, secara teknis juga tidak ditentukan, tetapi ada beberapa perselisihan.


Hal pertama yang harus dipahami adalah mengapa perilaku tidak terdefinisi dengan merujuk pointer nol. Di C ++ 03, sebenarnya ada sedikit ambiguitas di sini.

Meskipun "dereferensi penunjuk nol menghasilkan perilaku yang tidak ditentukan" disebutkan dalam catatan di §1.9 / 4 dan §8.3.2 / 4, hal itu tidak pernah dinyatakan secara eksplisit. (Catatan tidak normatif.)

Namun, seseorang dapat mencoba menyimpulkannya dari §3.10 / 2:

Nilai l mengacu pada objek atau fungsi.

Saat melakukan dereferensi, hasilnya adalah nilai l. Pointer nol tidak merujuk ke objek, oleh karena itu ketika kita menggunakan lvalue kita memiliki perilaku yang tidak terdefinisi. Masalahnya adalah kalimat sebelumnya tidak pernah disebutkan, jadi apa artinya "menggunakan" lvalue? Bahkan hanya menghasilkannya sama sekali, atau menggunakannya dalam arti yang lebih formal yaitu melakukan konversi lvalue-to-rvalue?

Terlepas dari itu, pasti tidak dapat dikonversi ke nilai r (§4.1 / 1):

Jika objek yang dirujuk nilai l bukan objek tipe T dan bukan objek dari tipe yang diturunkan dari T, atau jika objek tidak diinisialisasi, program yang memerlukan konversi ini memiliki perilaku yang tidak ditentukan.

Ini jelas perilaku yang tidak terdefinisi.

Ambiguitas berasal dari apakah perilaku tidak terdefinisi menjadi deference atau tidak, tetapi tidak menggunakan nilai dari penunjuk yang tidak valid (yaitu, dapatkan nilai l tetapi tidak mengubahnya menjadi nilai r). Jika tidak, maka int *i = 0; *i; &(*i);sudah didefinisikan dengan baik. Ini adalah masalah aktif .

Jadi kita memiliki tampilan "dereferensi null pointer, dapatkan perilaku tidak terdefinisi" dan tampilan "lemah" menggunakan pointer null yang terdefinisi, dapatkan perilaku tidak terdefinisi ".

Sekarang kita pertimbangkan pertanyaannya.


Ya, (a)menghasilkan perilaku tidak terdefinisi. Faktanya, jika thisnull maka terlepas dari konten fungsinya , hasilnya tidak ditentukan.

Ini mengikuti dari §5.2.5 / 3:

Jika E1memiliki tipe "penunjuk ke kelas X", ekspresi E1->E2tersebut akan diubah ke bentuk yang setara(*(E1)).E2;

*(E1)akan menghasilkan perilaku tidak terdefinisi dengan interpretasi yang ketat, dan .E2mengubahnya menjadi nilai r, menjadikannya perilaku tak terdefinisi untuk interpretasi yang lemah.

Ini juga mengikuti bahwa itu perilaku yang tidak ditentukan langsung dari (§9.3.1 / 1):

Jika fungsi anggota nonstatic dari kelas X dipanggil untuk objek yang bukan tipe X, atau tipe turunan dari X, perilakunya tidak ditentukan.


Dengan fungsi statis, interpretasi yang ketat versus lemah membuat perbedaan. Sebenarnya, ini tidak ditentukan:

Anggota statis dapat dirujuk menggunakan sintaks akses anggota kelas, dalam hal ini ekspresi objek dievaluasi.

Artinya, itu dievaluasi seolah-olah itu non-statis dan kami sekali lagi dereferensi pointer nol dengan (*(E1)).E2.

Namun, karena E1tidak digunakan dalam panggilan fungsi-anggota statis, jika kita menggunakan interpretasi yang lemah, panggilan tersebut didefinisikan dengan baik. *(E1)menghasilkan nilai l, fungsi statis diselesaikan, *(E1)dibuang, dan fungsi dipanggil. Tidak ada konversi lvalue-to-rvalue, jadi tidak ada perilaku yang tidak ditentukan.

Di C ++ 0x, pada n3126, ambiguitas tetap ada. Untuk saat ini, berhati-hatilah: gunakan interpretasi yang ketat.

GManNickG
sumber
5
+1. Melanjutkan pedantry, di bawah "definisi lemah", fungsi anggota nonstatis belum dipanggil "untuk objek yang bukan tipe X". Itu telah dipanggil untuk nilai l yang bukan merupakan objek sama sekali. Jadi solusi yang diusulkan menambahkan teks "atau jika nilai l adalah nilai l kosong" ke klausa yang Anda kutip.
Steve Jessop
Bisakah Anda menjelaskan sedikit? Secara khusus, dengan tautan "masalah tertutup" dan "masalah aktif" Anda, berapa nomor masalahnya? Juga, jika ini adalah masalah tertutup, apa sebenarnya jawaban ya / tidak untuk fungsi statis? Saya merasa seperti saya melewatkan langkah terakhir dalam mencoba memahami jawaban Anda.
Brooks Moses
4
Menurut saya, CWG defect 315 tidak "tertutup" seperti yang disiratkan oleh kehadirannya di halaman "masalah tertutup". Alasannya mengatakan bahwa itu harus diizinkan karena " *pbukan kesalahan ketika pnol kecuali nilai l diubah menjadi nilai r." Namun, hal itu bergantung pada konsep "nilai l kosong", yang merupakan bagian dari resolusi yang diusulkan untuk CWG cacat 232 , tetapi belum diadopsi. Jadi, dengan bahasa di C ++ 03 dan C ++ 0x, dereferensi penunjuk nol masih belum ditentukan, bahkan jika tidak ada konversi lvalue-to-rvalue.
James McNellis
1
@JamesMcNellis: Menurut pemahaman saya, jika pada alamat perangkat keras yang akan memicu beberapa tindakan saat dibaca, tetapi tidak dideklarasikan volatile, pernyataan *p;itu tidak akan diperlukan, tetapi akan diizinkan , untuk benar-benar membaca alamat itu; pernyataan tersebut &(*p);, bagaimanapun, akan dilarang untuk dilakukan. Jika *pberada volatile, membaca akan diperlukan. Dalam kedua kasus, jika penunjuk tidak valid, saya tidak dapat melihat bagaimana pernyataan pertama tidak akan menjadi Perilaku Tidak Terdefinisi, tetapi saya juga tidak dapat melihat mengapa pernyataan kedua akan menjadi.
supercat
1
".E2 mengubahnya menjadi nilai r," - Eh, tidak
MM
30

Jelas tidak terdefinisi artinya tidak terdefinisi , namun terkadang dapat diprediksi. Informasi yang akan saya berikan tidak boleh diandalkan untuk kode yang berfungsi karena tentu saja tidak dijamin, tetapi mungkin berguna saat melakukan debug.

Anda mungkin berpikir bahwa memanggil fungsi pada penunjuk objek akan mendereferensi penunjuk dan menyebabkan UB. Dalam praktiknya jika fungsinya bukan virtual, kompilator akan mengubahnya menjadi panggilan fungsi biasa dengan meneruskan pointer sebagai parameter pertama ini , melewati dereferensi dan membuat bom waktu untuk fungsi anggota yang dipanggil. Jika fungsi anggota tidak mereferensikan variabel anggota atau fungsi virtual apa pun, itu mungkin benar-benar berhasil tanpa kesalahan. Ingatlah bahwa kesuksesan termasuk dalam alam semesta yang "tidak ditentukan"!

Fungsi MFC Microsoft GetSafeHwnd sebenarnya bergantung pada perilaku ini. Saya tidak tahu apa yang mereka merokok.

Jika Anda memanggil fungsi virtual, penunjuk harus didereferensi untuk sampai ke vtable, dan pasti Anda akan mendapatkan UB (mungkin macet tapi ingat bahwa tidak ada jaminan).

Mark Ransom
sumber
1
GetSafeHwnd pertama melakukan! Pemeriksaan ini dan jika benar, mengembalikan NULL. Kemudian dimulai bingkai SEH dan referensi penunjuk. jika ada Pelanggaran Akses Memori (0xc0000005) ini tertangkap dan NULL dikembalikan ke pemanggil :) Jika tidak, HWND dikembalikan.
Петър Петров
@ ПетърПетров sudah beberapa tahun sejak saya melihat kodenya GetSafeHwnd, mungkin saja mereka telah meningkatkannya sejak saat itu. Dan jangan lupa bahwa mereka memiliki pengetahuan orang dalam tentang cara kerja compiler!
Markus Ransom
Saya menyatakan contoh penerapan yang mungkin memiliki efek yang sama, yang sebenarnya dilakukannya adalah direkayasa balik menggunakan debugger :)
Петър Петров
1
"mereka memiliki pengetahuan orang dalam tentang cara kerja kompilator!" - penyebab masalah abadi untuk proyek seperti MinGW yang mencoba mengizinkan g ++ untuk mengompilasi kode yang memanggil API Windows
MM
@ MM Saya pikir kita semua akan setuju ini tidak adil. Dan karena itu, saya juga berpikir ada undang-undang tentang kompatibilitas yang membuatnya agak ilegal untuk menyimpannya.
v.oddou