Czy możliwe jest odwołanie zerowe?

102

Czy ten fragment kodu jest prawidłowy (i zdefiniowane zachowanie)?

int &nullReference = *(int*)0;

Zarówno g ++ i brzęk ++ skompilować bez ostrzeżenia, nawet podczas używania -Wall, -Wextra, -std=c++98, -pedantic, -Weffc++...

Oczywiście referencja nie jest tak naprawdę pusta, ponieważ nie można uzyskać do niej dostępu (oznaczałoby to wyłuskiwanie pustego wskaźnika), ale możemy sprawdzić, czy jest pusta, czy nie, sprawdzając jej adres:

if( & nullReference == 0 ) // null reference
peoro
źródło
1
Czy możesz podać przypadek, w którym byłoby to przydatne? Innymi słowy, czy to tylko kwestia teorii?
cdhowie
Cóż, czy referencje są kiedykolwiek niezbędne? Zamiast nich można zawsze użyć wskaźników. Takie zerowe odniesienie pozwoliłoby ci użyć odwołania również wtedy, gdy nie możesz mieć żadnego obiektu, do którego można by się odwołać. Nie wiem, jak bardzo jest brudny, ale zanim o nim pomyślałem, interesowało mnie jego legalność.
peoro
8
Myślę, że to źle widziane
Domyślnie
22
„moglibyśmy sprawdzić” - nie, nie możesz. Istnieją kompilatory, które zmieniają instrukcję w if (false), eliminując sprawdzanie, właśnie dlatego, że odwołania i tak nie mogą być zerowe. Lepiej udokumentowana wersja istniała w jądrze Linuksa, gdzie zoptymalizowano bardzo podobne sprawdzenie NULL: isc.sans.edu/diary.html?storyid=6820
MSalters
2
„jednym z głównych powodów używania referencji zamiast wskaźnika jest uwolnienie się od ciężaru testowania w celu sprawdzenia, czy odnosi się do prawidłowego obiektu” ta odpowiedź w linku Default brzmi całkiem nieźle!
peoro,

Odpowiedzi:

75

Odnośniki nie są wskazówkami.

8.3.2 / 1:

Odniesienie powinno zostać zainicjowane w celu odniesienia się do prawidłowego obiektu lub funkcji. [Uwaga: w szczególności odwołanie zerowe nie może istnieć w dobrze zdefiniowanym programie, ponieważ jedynym sposobem utworzenia takiego odniesienia byłoby powiązanie go z „obiektem” uzyskanym przez dereferencję pustego wskaźnika, co powoduje niezdefiniowane zachowanie. Jak opisano w 9.6, odwołanie nie może być bezpośrednio powiązane z polem bitowym. ]

1,9 / 4:

Pewne inne operacje są opisane w niniejszej Normie Międzynarodowej jako niezdefiniowane (na przykład efekt wyłuskiwania wskaźnika zerowego)

Jak mówi Johannes w usuniętej odpowiedzi, istnieją pewne wątpliwości, czy „wyłuskiwanie wskaźnika zerowego” powinno zostać kategorycznie uznane za niezdefiniowane zachowanie. Nie jest to jednak jeden z przypadków budzących wątpliwości, ponieważ pusty wskaźnik z pewnością nie wskazuje na „prawidłowy obiekt lub funkcję”, a komitet normalizacyjny nie pragnie wprowadzać zerowych odniesień.

Steve Jessop
źródło
Usunąłem swoją odpowiedź, ponieważ zdałem sobie sprawę, że sama kwestia wyłuskiwania wskaźnika zerowego i uzyskania l-wartości, która się do niego odnosi, jest inną rzeczą niż faktyczne wiązanie odniesienia do niego, jak wspomniałeś. Chociaż mówi się, że lvalues ​​odnoszą się również do obiektów lub funkcji (więc w tym miejscu tak naprawdę nie ma różnicy w stosunku do powiązania referencyjnego), te dwie rzeczy nadal są oddzielnymi problemami. W przypadku samego aktu dereferencji, oto link: open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1102
Johannes Schaub - litb
1
@MSalters (odpowiedź na komentarz do usuniętej odpowiedzi; istotne tutaj) Nie mogę się szczególnie zgodzić z przedstawioną tam logiką. Chociaż wygodne może być odrzucenie &*pjako puniwersalne, nie wyklucza to nieokreślonego zachowania (które z natury może „wydawać się działać”); i nie zgadzam się, że typeidwyrażenie, które ma na celu określenie typu „wyłuskanego wskaźnika zerowego” w rzeczywistości usuwa odwołanie do wskaźnika zerowego. Widziałem ludzi, którzy poważnie się kłócili, na czym &a[size_of_array]nie można i nie należy polegać, a poza tym pisanie jest łatwiejsze i bezpieczniejsze a + size_of_array.
Karl Knechtel
@Default Standardy w tagach [c ++] powinny być wysokie. Moja odpowiedź brzmiała tak, jakby oba akty były jednym i tym samym :) Podczas dereferencji i uzyskiwania wartości l nie można pominąć, które odnosi się do „żadnego obiektu”, mogłoby być wykonalne, przechowywanie go w referencji wymyka się z tego ograniczonego zakresu i nagle może mieć wpływ znacznie więcej kodu.
Johannes Schaub - litb
@Karl dobrze w C ++ „wyłuskiwanie” nie oznacza czytania wartości. Niektórzy ludzie myślą, że „wyłuskiwanie” oznacza faktyczny dostęp lub modyfikację przechowywanej wartości, ale to nieprawda. Logika jest taka, że ​​C ++ mówi, że lwartość odnosi się do „obiektu lub funkcji”. Jeśli tak jest, to pojawia się pytanie, do czego *podnosi się lwartość , kiedy pjest pustym wskaźnikiem. C ++ obecnie nie ma pojęcia pustej wartości l, którą chciał wprowadzić problem.
Johannes Schaub - litb
Wykrywanie wyłuskanych wskaźników zerowych w typeidpracach opartych na składni, a nie na semantyce. Oznacza to, że jeśli nie typeid(0, *(ostream*)0)możesz zrobić mają niezdefiniowanej zachowanie - nie bad_typeidjest gwarantowana do rzucania, nawet jeśli przechodzą lwartością wynikającej z pustego wskaźnika dereference semantycznie. Ale składniowo na najwyższym poziomie nie jest to wyłuskiwanie, ale wyrażenie operatora przecinka.
Johannes Schaub - litb
26

Odpowiedź zależy od twojego punktu widzenia:


Jeśli oceniasz według standardu C ++, nie możesz uzyskać zerowego odwołania, ponieważ najpierw otrzymujesz niezdefiniowane zachowanie. Po tym pierwszym przypadku niezdefiniowanego zachowania norma zezwala na wszystko. Tak więc, jeśli piszesz *(int*)0, masz już niezdefiniowane zachowanie, tak jak jesteś, z punktu widzenia standardu języka, dereferencjonowanie pustego wskaźnika. Reszta programu jest nieistotna, po wykonaniu tego wyrażenia wypadasz z gry.


Jednak w praktyce odwołania o wartości null można łatwo utworzyć na podstawie wskaźników o wartości null i nie zauważysz tego, dopóki faktycznie nie spróbujesz uzyskać dostępu do wartości za odwołaniem zerowym. Twój przykład może być nieco zbyt prosty, ponieważ każdy dobry optymalizujący kompilator zobaczy niezdefiniowane zachowanie i po prostu zoptymalizuje wszystko, co od niego zależy (odniesienie zerowe nawet nie zostanie utworzone, zostanie zoptymalizowane).

Jednak ta optymalizacja zależy od kompilatora, aby udowodnić niezdefiniowane zachowanie, co może nie być możliwe. Rozważ tę prostą funkcję w pliku converter.cpp:

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

Kiedy kompilator widzi tę funkcję, nie wie, czy wskaźnik jest wskaźnikiem zerowym, czy nie. Więc po prostu generuje kod, który zamienia dowolny wskaźnik w odpowiednie odniesienie. (Btw: to jest noop, ponieważ wskaźniki i referencje są dokładnie tą samą bestią w asemblerze.) Teraz, jeśli masz inny plik user.cppz kodem

#include "converter.h"

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

kompilator nie wie, że toReference()wyłuskuje przekazany wskaźnik i zakłada, że ​​zwraca prawidłowe odwołanie, które w praktyce będzie odwołaniem zerowym. Wywołanie kończy się powodzeniem, ale gdy próbujesz użyć odwołania, program ulega awarii. Ufnie. Standard pozwala na wszystko, w tym pojawienie się różowych słoni.

Możesz zapytać, dlaczego jest to istotne, w końcu niezdefiniowane zachowanie zostało już wywołane w środku toReference(). Odpowiedź brzmi: debugowanie: zerowe odwołania mogą się propagować i mnożyć, tak jak robią to puste wskaźniki. Jeśli nie jesteś świadomy tego, że mogą istnieć odwołania zerowe i nauczysz się ich unikać, możesz spędzić trochę czasu próbując dowiedzieć się, dlaczego funkcja członkowska wydaje się zawieszać, gdy tylko próbuje odczytać zwykły stary intelement członkowski (odpowiedź: instancja w wywołaniu członka była odwołaniem o wartości null, więc thisjest to wskaźnik zerowy, a Twój element członkowski jest obliczany jako adres 8).


A co powiesz na sprawdzenie zerowych odwołań? Podałeś linię

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

w twoim pytaniu. Cóż, to nie zadziała: zgodnie ze standardem, masz niezdefiniowane zachowanie, jeśli wyłuskujesz pusty wskaźnik i nie możesz utworzyć zerowego odniesienia bez wyłuskiwania pustego wskaźnika, więc zerowe odwołania istnieją tylko w sferze niezdefiniowanego zachowania. Ponieważ Twój kompilator może założyć, że nie wyzwalasz niezdefiniowanego zachowania, może założyć, że nie ma czegoś takiego jak odwołanie o wartości null (nawet jeśli z łatwością wyemituje kod, który generuje odwołania o wartości null!). W związku z tym widzi if()warunek, stwierdza, że ​​nie może być prawdziwy, i po prostu odrzuca całe if()stwierdzenie. Wraz z wprowadzeniem optymalizacji czasu łącza sprawdzenie zerowych odwołań w solidny sposób stało się po prostu niemożliwe.


TL; DR:

Puste odniesienia są trochę upiorne:

Ich istnienie wydaje się niemożliwe (= standardowo),
ale istnieją (= przez wygenerowany kod maszynowy),
ale nie możesz ich zobaczyć, jeśli istnieją (= Twoje próby zostaną zoptymalizowane),
ale i tak mogą cię zabić nieświadomego (= Twój program ulega awarii w dziwnych momentach lub gorzej).
Twoją jedyną nadzieją jest to, że one nie istnieją (= napisz swój program, aby ich nie tworzyć).

Mam nadzieję, że nie będzie cię to prześladować!

cmaster - przywróć monikę
źródło
2
Czym dokładnie jest „pingowany słoń”?
Pharap
2
@Pharap Nie mam pojęcia, to była tylko literówka. Ale standard C ++ nie
dbałby o
9

Jeśli twoim zamiarem było znalezienie sposobu na reprezentowanie wartości null w wyliczaniu pojedynczych obiektów, to złym pomysłem jest (de) odwołanie do wartości null (to C ++ 11, nullptr).

Dlaczego nie zadeklarować statycznego obiektu pojedynczego, który reprezentuje wartość NULL w klasie, w następujący sposób i dodać operator rzutowania na wskaźnik, który zwraca wartość nullptr?

Edycja: Poprawiono kilka błędów i dodałem instrukcję if w main (), aby sprawdzić, czy operator rzutowania na wskaźnik faktycznie działa (o czym zapomniałem ... mój zły) - 10 marca 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
źródło
ponieważ jest wolny
wandalen
6

clang ++ 3.5 ostrzega nawet przed tym:

/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
źródło