Unikaj NULL

Opublikowane: 07/11/2015

w kategorii Zasoby.

Wstęp

Wartość null (NULL lub nullptr) w C++ może być użyta po to aby zasygnalizować że:

  • wartość nie jest jeszcze przypisana (i może nie będzie) lub
  • parametr nie został podany (wskaźnik z wartością domyślną = NULL)
  • wynik jest pusty (zwrócono null)

Jednakże taka sygnalizacja może być weryfikowana lub nie. Jeśli kod wywołujący nie sprawdzi wartości lub jej nie poda kiedy jest wymagana (poda null we wskaźniku), program ulegnie awarii w czasie run-time. Czasami jest to nieakceptowalna sytuacja, czasami sprawia to że analizowania innych problemów staje się bardziej skomplikowane. Dodatkowo, deklaracja parametrów funkcji jako wskaźników niekoniecznie oznacza, że możesz przesyłać null do tych parametrów.

Przed C++11 wartość null (NULL) była kompatybilna z typem integer, dlatego, kiedy miałeś dwa przeciążenia funkcji - dla int i wskaźnika, przesłanie NULL mogło wywołać pierwsze przeciążenie. Zobacz "Preferuj nullptr zamiast 0 i NULL" w "Skuteczny nowoczesny C++" napisane przez Scotta Meyersa (zobacz moją recenzję).

Aby ułatwić sobie życie, możesz spróbować ograniczyć używanie wartości nill w swoim programie używając następujących rozwiązań i technik.

Ogranicz używanie wskaźników

Zastąp wskaźniki referencjami kiedy parametry są wymagane. To jest łatwe rozwiązanie, ale strona wywołująca nadal może przesłać wartość null.

Uwaga: muszę to trochę rozwinąć, podczas mojej dyskusji z programistami zrozumiałem że zdanie powyżej nie jest wystarczająco jasne. Niektórzy twierdzą że to niemożliwe lub zabronione, inni mówią że jestem wariatem (delikatnie mówiąc), więc przykład poniżej:

#include <iostream>

using namespace std;

char *boo(int a) {
    if (a < 5)
        return "zasieg OK";
    else
        return nullptr;
}

void foo(char &cref, int b) {
    cout << "foo uruchomione" << endl;

    char &valueToDisplay = cref;
    cout << "referencja przeczytana" << endl;

    if (&cref == nullptr) 
        cout << "Uwaga: cref wskazuje na NULL!" << endl;

    cout << "wartosc znakowa: " << flush;
    cout << valueToDisplay;

    cout << "\nfoo zakonczone\n";
}

int main()
{
    int a;

    do {
        cout << "Wprowadz a: " << endl;
        if (!(cin >> a))
          break;

        cout << "--- Uruchamiam dla: " << a << " ---" << endl;
        char *cptr = boo(a);

        cout << "boo() zakonczone" << endl;
        char &cref = *cptr;

        cout << "referencja przygotowana do uzycia" << endl;
        int b = a + 3;

        foo(cref, b);

    } while(true);

    return 0;
}

Dla wejścia

0
5

Program generuje wynik:

Wprowadz a: 
--- Uruchamiam dla: 0 ---
boo() zakonczone
referencja przygotowana do uzycia
foo uruchomione
referencja przeczytana
wartosc znakowa: z
foo zakonczone
Wprowadz a: 
--- Uruchamiam dla: 5 ---
boo() zakonczone
referencja przygotowana do uzycia
foo uruchomione
referencja przeczytana
Uwaga: cref wskazuje na NULL!
wartosc znakowa:

Jak widzisz, program pracuje dopóki nie spróbuje odczytać wartości z "valueToDisplay". Wtedy ulega awarii, jeśli cref wskazuje na NULL. Tak więc program nie przerwie działania podczas konwersji ze wskaźnika do referencji. Także program nie przerwie działania podczas odczytu referencji-do-NULL. Ulegnie awarii tylko podczas czytania wartości wskazywanej przez referencję. Nie wcześniej.

Dlaczego? Ponieważ referencja jest tylko formą wskaźnika. Możesz to zobaczyć w kodzie wynikowym ASM dla linii konwersji wskaźnika-do-referencji - nie ma tam nic w kodzie ASM dla nich:

; 41   :         cout << "boo() completed" << endl;

    call    ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
    add esp, 4
    mov ecx, eax
    call    DWORD PTR __imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z

; 42   :         char &cref = *cptr;

; ---> niczego tu nie ma <---

; 43   :    
; 44   :         cout << "reference prepared for use" << endl;

    push    OFFSET ??$endl@DU?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@@Z ; std::endl<char,std::char_traits<char> >
    push    ecx
    mov ecx, DWORD PTR __imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A
    mov edx, OFFSET ??_C@_0BL@ENAPADAG@reference?5prepared?5for?5use?$AA@
    call    ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
    add esp, 4
    mov ecx, eax
    call    DWORD PTR __imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z

Tak więc owszem, możesz przesłać NULL/nullptr do referencji.

Możesz to zrobić nawet z tak prostym kodem jak poniżej:

char &cptr = *static_cast<char *>(NULL); // kompiluje się poprawnine (bez błędów czy ostrzeżeń) na VS2015

Możesz sprawdzić wskaźnik przed konwersją na referencję. Możesz sprawdzić referencję po konwersji, przed odczytem, wewnątrz foo(). Ale czasami ktoś zapomina o tym, wtedy, jeśli foo() jest wystarczająco skomplikowane, będziesz miał(a) kłopoty.

Możesz próbować zmusić wywołujący kod do wykonania sprawdzenia NULL - poprzez użycie klas "optional" lub "not_null" - zobacz opis poniżej.

Użyj klasy "optional"

Klasa "Optional" może być użyta do wskazania że parametr może być podany lub nie.

Istnieją dwa rozwiązania które znam na dzień dzisiejszy: Boost.Optional oraz std::experimental::optional.

Przykład dla Boost:

class Student {
public:
  Student(const std::string &name, int age): name_(name), age_(age) {}
  virtual std::string getName() { return name_; }
  virtual int getAge() { return age_; }
protected:
  std::string name_;
  int age_;
}


bool enroll_course_participant(const boost::optional<Student> &student) {
  if (student && (student->getAge() >= get_minimum_age_for_enroll()))
    return enroll_student(student.get()); 
  else 
    return false; 
}

Aby zwrócić opcjonalny wynik możesz użyć poniższego kodu jako przykładu:

boost::optional<Student> find_best_student() {
  if (list_.size() > 0)
    return list_[0];
  else
    return boost::optional<Student>();
}

cout << "Best student:\n";

boost::optional<Student> best = find_best_student();

if (best) 
  show_student(best)
else
  cout << "Best student not found\n";

Użyj klasy "not_null"

Podobne do optional ale z odwrotnym znaczeniem: parametr musi być podany jako wartość nie-null (parametr jest wymagany i podawany jako wskaźnik).

Zaimplementowane w GSL i może być użyte jak poniżej:

void print_int(not_null<int *> intp) {
  cout << intp.get() << endl;
}

int a = 3;
int *aptr = &a;
print_int(aptr);

Wzorzec Obiektu Null

Zamiast przesyłania wartości null możesz przesłać obiekt który implementuje wszystkie funkcje w sposób kompatybilny z API klasy, ale który nie zawiera żadnych wartości.

Przykład:

class StudentIntf {
public:
  virtual ~StudentIntf() {} // required for interface classes 
  virtual std::string getName() = 0;  
  virtual int getAge() = 0;  
}

class Student: public StudentIntf {
public:
  Student(const std::string &name, int age): name_(name), age_(age) {}
  virtual std::string getName() { return name_; }
  virtual int getAge() { return age_; }
protected:
  std::string name_;
  int age_;
}

class StudentNull: public StudentIntf {
public:
  virtual std::string getName() { return ""; }
  virtual int getAge() { return 0; }
}

bool enroll_course_participant(const StudentIntf &student) {
  if (student.getAge() >= get_minimum_age_for_enroll())
    return enroll_student(student);


  else 
    return false; 
}

void show_student(const StudentIntf &student) {
   cout << "Student: " << student.getName() << " is in age of " << student.getAge() << "\n";
}

Możesz użyć tego wzorca także gdy nie masz żadnej wartości do zwrócenia.

StudentIntf find_best_student() {
  if (list_.size() > 0)
    return list_[0];
  else
    return StudentNull();
}

cout << "Best student:\n";
show_student(find_best_student());

Zobacz również

Udostępnij

obserwuj