Apakah referensi nol mungkin?

102

Apakah potongan kode ini valid (dan perilaku yang ditentukan)?

int &nullReference = *(int*)0;

Kedua g ++ dan dentang ++ kompilasi tanpa peringatan apapun, bahkan ketika menggunakan -Wall, -Wextra, -std=c++98, -pedantic, -Weffc++...

Tentu saja referensi sebenarnya tidak null, karena tidak dapat diakses (itu berarti mendereferensi pointer nol), tetapi kita dapat memeriksa apakah itu null atau tidak dengan memeriksa alamatnya:

if( & nullReference == 0 ) // null reference
peoro
sumber
1
Dapatkah Anda memberikan kasus di mana ini sebenarnya akan berguna? Dengan kata lain, apakah ini hanya pertanyaan teori?
cdhowie
Nah, apakah referensi sangat diperlukan? Pointer selalu dapat digunakan sebagai pengganti mereka. Referensi null seperti itu akan memungkinkan Anda menggunakan referensi juga saat Anda tidak memiliki objek untuk dirujuk. Entah seberapa kotornya, tapi sebelum memikirkannya saya tertarik dengan legalitasnya.
peoro
8
Saya pikir itu tidak disukai
Default
22
"kita bisa memeriksa" - tidak, kamu tidak bisa. Ada kompiler yang mengubah pernyataan menjadi if (false), menghilangkan centang, justru karena referensi tidak boleh kosong. Versi terdokumentasi yang lebih baik ada di kernel Linux, di mana pemeriksaan NULL yang sangat mirip telah dioptimalkan: isc.sans.edu/diary.html?storyid=6820
MSalters
2
"salah satu alasan utama untuk menggunakan referensi alih-alih penunjuk adalah untuk membebaskan Anda dari beban keharusan menguji untuk melihat apakah itu merujuk ke objek yang valid" jawaban ini, dalam tautan Default, terdengar cukup bagus!
peoro

Jawaban:

75

Referensi bukanlah petunjuk.

8.3.2 / 1:

Referensi harus diinisialisasi untuk merujuk ke objek atau fungsi yang valid. [Catatan: secara khusus, referensi null tidak bisa ada dalam program yang didefinisikan dengan baik, karena satu-satunya cara untuk membuat referensi seperti itu adalah dengan mengikatnya ke "objek" yang diperoleh dengan mendereferensi pointer nol, yang menyebabkan perilaku tidak terdefinisi. Seperti yang dijelaskan di 9.6, referensi tidak bisa terikat langsung ke bidang bit. ]

1,9 / 4:

Operasi tertentu lainnya dijelaskan dalam Standar Internasional ini sebagai tidak ditentukan (misalnya, efek dereferensi penunjuk nol)

Seperti yang Yohanes katakan dalam jawaban yang dihapus, ada beberapa keraguan apakah "mendereferensi penunjuk nol" harus secara kategoris dinyatakan sebagai perilaku yang tidak terdefinisi. Tapi ini bukan salah satu kasus yang menimbulkan keraguan, karena penunjuk nol jelas tidak menunjuk ke "objek atau fungsi yang valid", dan tidak ada keinginan dalam komite standar untuk memperkenalkan referensi nol.

Steve Jessop
sumber
Saya menghapus jawaban saya karena saya menyadari bahwa masalah dereferencing pointer nol dan mendapatkan nilai l yang merujuk ke itu adalah hal yang berbeda daripada sebenarnya mengikat referensi ke sana, seperti yang Anda sebutkan. Meskipun lvalues ​​dikatakan merujuk ke objek atau fungsi juga (jadi pada poin ini, sebenarnya tidak ada perbedaan pada pengikatan referensi), kedua hal ini masih menjadi perhatian yang terpisah. Untuk tindakan dereferensi belaka, inilah tautannya: open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1102
Johannes Schaub - litb
1
@MSalters (balas untuk mengomentari jawaban yang dihapus; relevan di sini) Saya sangat tidak setuju dengan logika yang disajikan di sana. Meskipun mungkin nyaman untuk diutarakan &*psecara puniversal, hal itu tidak mengesampingkan perilaku tidak terdefinisi (yang menurut sifatnya mungkin "tampak berhasil"); dan saya tidak setuju bahwa typeidekspresi yang berusaha untuk menentukan jenis "pointer nol dereferensi" sebenarnya dereferensi penunjuk nol. Saya telah melihat orang-orang berdebat dengan serius yang &a[size_of_array]tidak dapat dan tidak boleh diandalkan, dan bagaimanapun lebih mudah dan aman untuk hanya menulis a + size_of_array.
Karl Knechtel
@Default Standar dalam tag [c ++] harus tinggi. Jawaban saya terdengar seperti kedua tindakan itu satu dan hal yang sama :) Saat dereferencing dan mendapatkan nilai l yang tidak Anda sebarkan yang mengacu pada "tidak ada objek" mungkin layak, menyimpannya ke dalam pelarian referensi yang cakupannya terbatas dan tiba-tiba dapat berdampak lebih banyak kode.
Johannes Schaub - litb
Sangat cocok di C ++, "dereferencing" tidak berarti membaca nilai. Beberapa orang berpikir "dereferensi" berarti mengakses atau mengubah nilai yang disimpan, tetapi itu tidak benar. Logikanya adalah bahwa C ++ mengatakan bahwa nilai l mengacu pada "sebuah objek atau fungsi". Jika demikian, maka pertanyaannya adalah apa yang dirujuk oleh lvalue *p, kapan ppointer nol. C ++ saat ini tidak memiliki gagasan tentang nilai l kosong, yang ingin diperkenalkan oleh edisi 232.
Johannes Schaub - litb
Deteksi pointer nol yang didereferensi dalam typeidkarya berdasarkan sintaks, bukan berdasarkan semantik. Artinya, jika Anda melakukannya typeid(0, *(ostream*)0)Anda lakukan memiliki perilaku tidak terdefinisi - tidak ada bad_typeiddijamin untuk dilempar, meskipun Anda lulus lvalue yang dihasilkan dari pointer dereference nol semantik. Tapi secara sintaksis di tingkat atas, ini bukan dereferensi, melainkan ekspresi operator koma.
Johannes Schaub - litb
26

Jawabannya tergantung pada sudut pandang Anda:


Jika Anda menilai dengan standar C ++, Anda tidak bisa mendapatkan referensi null karena Anda mendapatkan perilaku tidak terdefinisi terlebih dahulu. Setelah kejadian pertama dari perilaku tidak terdefinisi, standar memungkinkan segala sesuatu terjadi. Jadi, jika Anda menulis *(int*)0, Anda sudah memiliki perilaku yang tidak terdefinisi seperti Anda, dari sudut pandang standar bahasa, mengabaikan pointer nol. Sisa program tidak relevan, setelah ungkapan ini dijalankan, Anda keluar dari permainan.


Namun, dalam praktiknya, referensi null dapat dengan mudah dibuat dari pointer null, dan Anda tidak akan menyadarinya sampai Anda benar-benar mencoba mengakses nilai di belakang referensi null. Contoh Anda mungkin agak terlalu sederhana, karena setiap kompiler pengoptimalan yang baik akan melihat perilaku tidak terdefinisi, dan hanya mengoptimalkan apa pun yang bergantung padanya (referensi nol bahkan tidak akan dibuat, itu akan dioptimalkan).

Namun, pengoptimalan tersebut bergantung pada compiler untuk membuktikan perilaku tidak terdefinisi, yang mungkin tidak dapat dilakukan. Pertimbangkan fungsi sederhana ini di dalam file converter.cpp:

int& toReference(int* pointer) {
    return *pointer;
}

Ketika kompilator melihat fungsi ini, ia tidak tahu apakah pointernya adalah pointer null atau bukan. Jadi itu hanya menghasilkan kode yang mengubah pointer apa pun menjadi referensi yang sesuai. (Btw: Ini adalah noop karena pointer dan referensi adalah binatang yang sama persis di assembler.) Sekarang, jika Anda memiliki file lain user.cppdengan kode

#include "converter.h"

void foo() {
    int& nullRef = toReference(nullptr);
    cout << nullRef;    //crash happens here
}

kompilator tidak tahu bahwa toReference()akan mendereferensi penunjuk yang diteruskan, dan menganggap bahwa ia mengembalikan referensi yang valid, yang dalam praktiknya akan menjadi referensi nol. Panggilan berhasil, tetapi ketika Anda mencoba menggunakan referensi, program lumpuh. Semoga. Standar tersebut memungkinkan terjadinya apa saja, termasuk penampilan gajah merah muda.

Anda mungkin bertanya mengapa ini relevan, lagipula, perilaku tidak terdefinisi sudah dipicu di dalam toReference(). Jawabannya adalah debugging: Referensi nol dapat menyebar dan berkembang biak seperti yang dilakukan pointer nol. Jika Anda tidak menyadari bahwa referensi null bisa ada, dan belajar untuk menghindari pembuatannya, Anda mungkin meluangkan cukup waktu untuk mencoba mencari tahu mengapa fungsi anggota Anda tampaknya macet ketika hanya mencoba membaca anggota lama yang biasa int(jawaban: contoh dalam panggilan anggota itu referensi nol, begitu thisjuga pointer nol, dan anggota Anda dihitung untuk ditempatkan sebagai alamat 8).


Jadi bagaimana dengan memeriksa referensi null? Anda memberi garis

if( & nullReference == 0 ) // null reference

dalam pertanyaan Anda. Nah, itu tidak akan berhasil: Menurut standar, Anda memiliki perilaku yang tidak terdefinisi jika Anda mendereferensi penunjuk nol, dan Anda tidak dapat membuat referensi nol tanpa mendereferensi penunjuk null, jadi referensi null hanya ada di dalam ranah perilaku tidak terdefinisi. Karena kompilator Anda mungkin berasumsi bahwa Anda tidak memicu perilaku yang tidak ditentukan, ia dapat berasumsi bahwa tidak ada yang namanya referensi null (meskipun ia dengan mudah akan mengeluarkan kode yang menghasilkan referensi null!). Dengan demikian, ia melihat if()kondisinya, menyimpulkan bahwa itu tidak mungkin benar, dan hanya membuang seluruh if()pernyataan. Dengan diperkenalkannya pengoptimalan waktu tautan, menjadi sangat tidak mungkin untuk memeriksa referensi null dengan cara yang kuat.


TL; DR:

Referensi kosong agaknya merupakan keberadaan yang mengerikan:

Keberadaan mereka tampaknya tidak mungkin (= menurut standar),
tetapi mereka ada (= oleh kode mesin yang dihasilkan),
tetapi Anda tidak dapat melihatnya jika ada (= upaya Anda akan dioptimalkan),
tetapi mereka mungkin membunuh Anda tanpa menyadarinya (= program Anda macet di titik-titik aneh, atau lebih buruk lagi).
Satu-satunya harapan Anda adalah mereka tidak ada (= tulis program Anda untuk tidak membuatnya).

Saya berharap hal itu tidak akan menghantui Anda!

cmaster - kembalikan monica
sumber
2
Apa sebenarnya "ping elephant"?
Pharap
2
@Pharap Saya tidak tahu, itu hanya salah ketik. Tetapi standar C ++ tidak akan peduli apakah itu gajah merah muda atau ping yang muncul, bagaimanapun ;-)
cmaster - kembalikan monica
9

Jika niat Anda adalah menemukan cara untuk merepresentasikan null dalam enumerasi objek tunggal, maka sebaiknya gunakan (de) referensi null (it C ++ 11, nullptr) sebagai ide yang buruk.

Mengapa tidak mendeklarasikan objek tunggal statis yang mewakili NULL dalam kelas sebagai berikut dan menambahkan operator cast-to-pointer yang mengembalikan nullptr?

Sunting: Memperbaiki beberapa kesalahan ketik dan menambahkan pernyataan-if di main () untuk menguji operator cast-to-pointer benar-benar berfungsi (yang saya lupa .. saya buruk) - 10 Maret 2015 -

// Error.h
class Error {
public:
  static Error& NOT_FOUND;
  static Error& UNKNOWN;
  static Error& NONE; // singleton object that represents null

public:
  static vector<shared_ptr<Error>> _instances;
  static Error& NewInstance(const string& name, bool isNull = false);

private:
  bool _isNull;
  Error(const string& name, bool isNull = false) : _name(name), _isNull(isNull) {};
  Error() {};
  Error(const Error& src) {};
  Error& operator=(const Error& src) {};

public:
  operator Error*() { return _isNull ? nullptr : this; }
};

// Error.cpp
vector<shared_ptr<Error>> Error::_instances;
Error& Error::NewInstance(const string& name, bool isNull = false)
{
  shared_ptr<Error> pNewInst(new Error(name, isNull)).
  Error::_instances.push_back(pNewInst);
  return *pNewInst.get();
}

Error& Error::NOT_FOUND = Error::NewInstance("NOT_FOUND");
//Error& Error::NOT_FOUND = Error::NewInstance("UNKNOWN"); Edit: fixed
//Error& Error::NOT_FOUND = Error::NewInstance("NONE", true); Edit: fixed
Error& Error::UNKNOWN = Error::NewInstance("UNKNOWN");
Error& Error::NONE = Error::NewInstance("NONE");

// Main.cpp
#include "Error.h"

Error& getError() {
  return Error::UNKNOWN;
}

// Edit: To see the overload of "Error*()" in Error.h actually working
Error& getErrorNone() {
  return Error::NONE;
}

int main(void) {
  if(getError() != Error::NONE) {
    return EXIT_FAILURE;
  }

  // Edit: To see the overload of "Error*()" in Error.h actually working
  if(getErrorNone() != nullptr) {
    return EXIT_FAILURE;
  }
}
David Lee
sumber
karena lambat
wandalen
6

clang ++ 3.5 bahkan memperingatkannya:

/tmp/a.C:3:7: warning: reference cannot be bound to dereferenced null pointer in well-defined C++ code; comparison may be assumed to
      always evaluate to false [-Wtautological-undefined-compare]
if( & nullReference == 0 ) // null reference
      ^~~~~~~~~~~~~    ~
1 warning generated.
Jan Kratochvil
sumber