Boş referans mümkün mü?


104

Bu kod parçası geçerli mi (ve tanımlanmış davranış)?

int &nullReference = *(int*)0;

Hem gr ++ ve çınlama ++ derleme onu herhangi bir uyarı olmadan, kullanırken bile -Wall, -Wextra, -std=c++98, -pedantic, -Weffc++...

Elbette referans, erişilemediği için aslında boş değildir (boş göstericiye başvurmak anlamına gelir), ancak adresini kontrol ederek boş olup olmadığını kontrol edebiliriz:

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

1
Bunun gerçekten yararlı olacağı bir durum verebilir misiniz? Başka bir deyişle, bu sadece bir teori sorusu mu?
cdhowie

Peki referanslar vazgeçilmez midir? Bunların yerine her zaman işaretçiler kullanılabilir. Böyle bir boş referans , başvuracak nesneniz olmadığında da bir referans kullanmanıza izin verir. Ne kadar kirli olduğunu bilmiyorum ama düşünmeden önce yasallığı ile ilgileniyordum.
peoro


22
"kontrol edebiliriz" - hayır, yapamazsınız. İfadeyi if (false), kontrolü ortadan kaldıran, tam da referanslar zaten boş olamayacağı için çeviren derleyiciler vardır . Linux çekirdeğinde, çok benzer bir NULL kontrolünün optimize edildiği daha iyi belgelenmiş bir sürüm vardı: isc.sans.edu/diary.html?storyid=6820
MSalters

2
"Bir işaretçi yerine bir referans kullanmanın en önemli nedenlerinden biri, sizi geçerli bir nesneye atıfta bulunup bulunmadığını görmek için test etme zorunluluğundan kurtarmaktır" Bu yanıt, Default'un bağlantısında oldukça iyi görünüyor!
peoro

Yanıtlar:


76

Referanslar işaretçi değildir.

8.3.2 / 1:

Geçerli bir nesneye veya işleve atıfta bulunmak için bir referans başlatılmalıdır. [Not: Özellikle, iyi tanımlanmış bir programda boş bir referans olamaz, çünkü böyle bir referans oluşturmanın tek yolu, onu, tanımsız davranışa neden olan bir boş göstericiye başvurarak elde edilen "nesneye" bağlamak olacaktır. 9.6'da açıklandığı gibi, bir referans doğrudan bir bit alanına bağlanamaz. ]

1.9 / 4:

Bu Uluslararası Standartta belirli diğer işlemler tanımsız olarak tanımlanmıştır (örneğin, boş göstericiye başvurunun kaldırılmasının etkisi)

Johannes'in silinmiş bir cevapta dediği gibi, "bir boş göstericinin referansını kaldırmanın" kategorik olarak tanımlanmamış bir davranış olarak belirtilmesi gerekip gerekmediği konusunda bazı şüpheler var. Ancak bu, şüphe uyandıran durumlardan biri değildir, çünkü bir boş gösterici kesinlikle "geçerli bir nesne veya işlevi" göstermez ve standartlar komitesi içinde boş referanslar sunma isteği yoktur.


Cevabımı kaldırdım çünkü bir boş göstericinin referansını kaldırıp buna atıfta bulunan bir değer elde etmenin, bahsettiğiniz gibi, aslında bir referansı bağlamadan farklı bir şey olduğunu fark ettim. Değerlerin de nesnelere veya işlevlere atıfta bulunduğu söylense de (bu noktada, referans bağlamada gerçekten bir fark yoktur), bu iki şey hala ayrı konulardır. Yalnızca başvurudan feragat
Johannes Schaub - litb

1
@MSalters (silinen cevapla ilgili yoruma cevap verin; burayla ilgili) Orada sunulan mantığa özellikle katılamıyorum. Evrensel &*polarak seçilmek uygun olsa da p, bu tanımlanmamış davranışları (doğası gereği "işe yarıyor" gibi görünebilir) dışlamaz; ve typeid"başvurulan boş gösterici" nin tipini belirlemeye çalışan bir ifadenin aslında boş göstericiye başvurduğunu kabul etmiyorum. İnsanların &a[size_of_array]güvenilmez ve güvenilmemesi gerektiğini ciddi bir şekilde tartıştıklarını gördüm ve zaten yazmak daha kolay ve güvenli a + size_of_array.
Karl Knechtel

[C ++] etiketlerindeki @Default Standards yüksek olmalıdır. Cevabım, her iki eylem de tek ve aynı şeymiş gibi geldi :) "Hiçbir nesneye" atıfta bulunan, atıfta bulunmadan ve etrafından geçmediğiniz bir değer elde ederken, bunu sınırlı kapsamı olan ve aniden etkileyebilecek bir referans kaçışına depolamak mümkün olabilir. çok daha fazla kod.
Johannes Schaub -

@Karl well in C ++, "dereferencing" bir değer okumak anlamına gelmez. Bazı insanlar "başvurunun kaldırılmasının" depolanan değere gerçekten erişmek veya onu değiştirmek anlamına geldiğini düşünür, ancak bu doğru değildir. Mantık, C ++ 'nın bir ldeğerin "bir nesne veya işlevi" ifade ettiğini söylemesidir. Eğer öyleyse, soru ldeğerin neyi kastettiğidir *p, ne zaman pbir boş gösterici olur. C ++ şu anda 232 numaralı sayının tanıtmak istediği boş bir değer kavramına sahip değildir.
Johannes Schaub -

typeidAnlambilim yerine sözdizimine dayalı çalışmalarda başvurulan boş işaretçilerin tespiti . Olduğunu, bunu yaparsanız typeid(0, *(ostream*)0)size do tanımsız davranışa sahip - hayır bad_typeidsen semantik bir boş gösterici dereference kaynaklanan bir lvalue geçmesi halde atılmasını garanti edilir. Ancak sözdizimsel olarak en üst düzeyde, bu bir referans değil, virgül operatör ifadesidir.
Johannes Schaub -

27

Cevap, bakış açınıza bağlıdır:


C ++ standardına göre karar verirseniz, önce tanımsız davranışı aldığınız için boş bir başvuru alamazsınız. Bu ilk tanımlanmamış davranış olayından sonra, standart her şeyin olmasına izin verir. Yani, yazarsanız *(int*)0, dil standardı bakış açısından, bir boş göstericiye başvurmadan başvurduğunuz gibi zaten tanımlanmamış bir davranışınız olur. Programın geri kalanı ilgisizdir, bu ifade bir kez uygulandığında oyunun dışında kalırsınız.


Bununla birlikte, pratikte boş referanslar, boş işaretçilerden kolayca oluşturulabilir ve boş referansın arkasındaki değere gerçekten erişmeye çalışana kadar fark etmezsiniz. Örneğiniz biraz fazla basit olabilir, çünkü herhangi bir iyi optimizasyon derleyicisi tanımlanmamış davranışı görecek ve ona bağlı olan her şeyi basitçe optimize edecek (boş referans oluşturulmayacak, optimize edilecek).

Yine de, bu optimizasyon, derleyicinin tanımlanmamış davranışı kanıtlamasına bağlıdır, ki bunu yapmak mümkün olmayabilir. Bir dosyanın içindeki şu basit işlevi düşünün converter.cpp:

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

Derleyici bu işlevi gördüğünde, göstericinin boş gösterici olup olmadığını bilmez. Böylece herhangi bir işaretçiyi karşılık gelen referansa dönüştüren bir kod üretir. (Btw: İşaretçiler ve referanslar assembler'daki aynı canavar olduğundan bu bir noop'tur.) Şimdi, eğer user.cppkodu içeren başka bir dosyanız varsa

#include "converter.h"

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

derleyici toReference(), aktarılan göstericiye başvurmayacağını bilmez ve pratikte boş bir başvuru olacak geçerli bir başvuru döndürdüğünü varsayar. Çağrı başarılı olur, ancak referansı kullanmaya çalıştığınızda program çöker. İnşallah. Standart, pembe fillerin görünümü dahil her şeyin olmasına izin verir.

Bunun neden alakalı olduğunu sorabilirsiniz, sonuçta, tanımlanmamış davranış zaten içeride tetiklendi toReference(). Cevap, hata ayıklamaktır: Boş referanslar tıpkı boş işaretçilerin yaptığı gibi yayılabilir ve çoğalabilir. Boş referansların var olabileceğinin farkında değilseniz ve bunları oluşturmaktan kaçınmayı öğrenirseniz, üye işlevinizin neden sadece eski bir intüyeyi okumaya çalışırken çöktüğünü anlamaya çalışmak için epey zaman harcayabilirsiniz (cevap: örnek üyenin çağrısında boş bir referans vardı, bu yüzden thisbir boş gösterici ve üyeniz adres 8 olarak hesaplanır).


Peki boş referansları kontrol etmeye ne dersiniz? Hattı sen verdin

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

sorunuzda. Pekala, bu işe yaramaz: Standarda göre, bir boş göstericiye başvurursanız tanımsız bir davranışınız olur ve bir boş göstericiye başvurmadan boş bir referans oluşturamazsınız, bu nedenle boş referanslar yalnızca tanımsız davranış alanı içinde bulunur. Derleyiciniz tanımsız davranışı tetiklemediğinizi varsayabileceğinden, boş referans diye bir şeyin olmadığını varsayabilir (boş referanslar üreten kodu kolayca yaysa bile!). Böyle olunca if()durumu görür, bunun doğru olamayacağı sonucuna varır ve tüm if()ifadeyi bir kenara atar . Bağlantı süresi optimizasyonlarının tanıtılmasıyla, boş referansları sağlam bir şekilde kontrol etmek imkansız hale geldi.


TL; DR:

Boş referanslar biraz korkunç bir varlıktır:

Varlıkları imkansız görünüyor (= standart olarak),
ancak varlar (= üretilen makine koduyla),
ancak var olup olmadıklarını göremezsiniz (= girişimleriniz ortadan kalkacak),
ancak yine de sizi farkında olmadan öldürebilirler (= programınız tuhaf noktalarda çöküyor veya daha kötüsü).
Tek umudunuz onların var olmamasıdır (= programınızı onları yaratmamak için yazın).

Umarım bu sana musallat olmaz!


2
"Ping fili" tam olarak nedir?
Pharap

2
@Pharap Hiçbir fikrim yok, sadece bir yazım hatası. Ancak C ++ standardı pembe mi yoksa ping filler mi göründüğünü
umursamıyor

9

Eğer amacınız, tekil nesnelerin bir numaralandırmasında null'u temsil etmenin bir yolunu bulmaksa, o zaman boşa (o C ++ 11, nullptr) başvurmak (de) kötü bir fikirdir.

Neden sınıf içinde NULL'yi temsil eden statik tekli nesneyi aşağıdaki gibi bildirip nullptr döndüren bir işaretçiye atama operatörü eklemiyorsunuz?

Düzenleme: Birkaç yanlış yazım düzeltildi ve işaretçiye atama operatörünün gerçekten çalıştığını test etmek için main () 'e if-ifadesi eklendi (bunu unuttum .. benim hatam) - 10 Mart 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;
  }
}

çünkü yavaş
wandalen

6

clang ++ 3.5 bununla ilgili uyarır:

/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.
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.