Avoid NULL

Overview

Null value (NULL or nullptr) can be used in C++ to indicate that:

  • value was not assigned yet (and maybe won't be assigned) or
  • parameter is not provided (pointer with default value = NULL)
  • result is empty (null returned)

However such an indication can be verified or not. If the calling code will fail to verify value or to provide value when it is required (passes null in pointer), program will fail in run-time. Sometimes it's not an acceptable situation, sometimes it just makes debugging other problems more complicated. Also, declaring function's parameters as pointers doesn't necessary mean you can pass null to these parameters.

Before C++11 null value (NULL) was compatible with integer, so when you had two function overloads - for int and pointer, passing NULL could invoke first overload. See "Prefer nullptr to 0 and NULL." in "Effective Modern C++" by Scott Meyers (see my review).

In order to ease your life, you can try to limit usage of null values in your program using the following solutions and techniques.

Limit usage of pointers

Replace pointers with references when parameters are required. This is easy solution, but caller can ignore it and still pass null value.

Note: I have to elaborate on this, during my discussion with developers I understood the sentence above was not self-explanatory. Some people say it's impossible or forbidden, others say I'm crazy (to put it mildly), so see the example below:

#include <iostream>

using namespace std;

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

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

    char &valueToDisplay = cref;
    cout << "reference read" << endl;

    if (&cref == nullptr) 
        cout << "Warning: cref points to NULL!" << endl;

    cout << "char value: " << flush;
    cout << valueToDisplay;

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

int main()
{
    int a;

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

        cout << "--- Running for: " << a << " ---" << endl;
        char *cptr = boo(a);

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

        cout << "reference prepared for use" << endl;
        int b = a + 3;

        foo(cref, b);

    } while(true);

    return 0;
}

For input

0
5

Program generates output:

Enter a: 
--- Running for: 0 ---
boo() completed
reference prepared for use
foo started
reference read
char value: r
foo completed
Enter a: 
--- Running for: 5 ---
boo() completed
reference prepared for use
foo started
reference read
Warning: cref points to NULL!
char value:

As you can see, program works until it tries to read value from "valueToDisplay". Then it fails if cref points to NULL. So program will NOT fail during conversion from pointer to reference. Also program will NOT fail during read of reference-to-NULL. It will only fail during read of value pointed by reference. It won't fail before.

Why? Because reference is just a kind of pointer. You can see that in ASM output for pointer-to-reference conversion lines - there is nothing in ASM for them:

; 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;

; ---> no code here <---

; 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

So yes, you can pass NULL/nullptr to reference.

You can do it even in such a simple code as below:

char &cptr = *static_cast<char *>(NULL); // compiles OK (no errors or warnings) on VS2015

You can check the pointer before conversion to reference. You can check the reference after conversion, before read, inside foo(). But sometimes somebody forgets to do that, then, if foo() is complicated enough, you will be in trouble.

You can try to force calling code to perform the NULL validation - by employing "optional" or "not_null" classes - see description below.

Use "optional" class

"Optional" class can be used to indicate that function parameter can be provided or not.

There are two solutions I'm aware of today: Boost.Optional and std::experimental::optional.

Simple example for 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; 
}

To return optional result you can use the following code as an example:

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";

Use "not_null" class

Similar to optional but with opposite meaning: parameter has to be provided as non-null value (parameter is required and passed as pointer).

It's implemented in GSL and can be used as below:

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

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

Null Object Pattern

Instead of passing null you can pass object which implements all functions in a way that it is compatible with class API, but does not contain any values.

Example:

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";
}

Also you can use this pattern to return value even if there is no record to return.

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

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

See also

Share

follow