How not_null can improve your code?

fenbf
14.6K views

Open Source Your Knowledge, Become a Contributor

Technology knowledge has to be shared and made accessible for free. Join the movement.

Create Content

How not_null can improve your code?

not_null examples

One of the key points of modern C++, as I observe, is to be expressive and use proper types. For example, regarding null pointers, rather than just writing a comment:

void Foo(int* pInt); // pInt cannot be null

I should actually use not_null<int *> pInt.

The code looks great now, isn't it? Let's investigate what not_null (from the Core Guidelines/Guideline Support Library) can do for us.

Intro

In your application, there are probably lots of places where you have to check if a pointer is not null before you process it. How many times do you write similar code:

if (pMyData)
    pMyData->Process();

or:

auto result = pObj ? pObj->Compute() : InvalidVal;

or

void Foo(Object* pObj)
{
    if (!pObj)
        return;

    // Do stuff...
}

What are the problems with the code?

  • It's error-prone: you might forget about if statements and then you might end up with AV (Memory Access Violation), or some other strange errors.
  • Code duplication
  • Error handling might be on a wrong level. Some functions must accept the null object, but some should depend on the caller to do the checks.
  • Performance hit. One additional check might not be a huge deal, but in some projects, I see hundreds or more of such tests.

What if we could forget about most of those safety checks and just be sure the pointer is always valid? How can we enforce such contract?

As you know, writing a simple comment, like "this argument cannot be null" won't do the job :)

There's a simple solution suggested in Core Guidelines:

I.12: Declare a pointer that must not be null as not_null.

So what's that not_null type? How can it help us?

The article was inspired mostly by Kate Gregory's original article: Using the not_null Template for Pointers That Must Never Be Nul. Moreover, Kate's done a great course about core guidelines, where she also experimented with not_null. Check it here: First Look: C++ Core Guidelines and the Guideline Support Library @Pluralsight.

The basics

not_null is a class that can wrap a pointer (or a smart pointer) and guarantees that it will hold only not null values.

The helper class can be found in the Guideline Support Library (GSL, not GLS :))

We can use Microsoft's implementation:

github.com/Microsoft/GSL/include/gsl/gsl

//
// not_null
//
// Restricts a pointer or smart pointer to only hold non-null values.

(Strangely the class itself is located not in a separate header but in the core header for GSL, so you cannot include only that class without including all other stuff. There's a reported issue that might solve that problem: #issue 502).

The basic idea is that you can write:

not_null<int *> pIntPtr = nullptr;

And you'll get a compile-time error as it's not possible to assign nullptr to the pointer. When you have such pointer, you can be sure it's valid and can be accessed.

For a function:

void Foo(not_null<Object*> pObj)
{
    // Do stuff...
}

Inside Foo you are guaranteed to have a valid pointer, and the additional checks might be removed.

That's some basic theory, and now let's consider a few more examples.

I divided examples into two sections: compile time and runtime. While it would be cool to handle nullptr at compile time only, we won't get away with issues happening at runtime.

Compile time

The wrapper class won't allow to construct a not_null object from nullptr, nor it allows to assign null. That's useful in several situations:

  • When you have not null pointer and want to clear it:
not_null<int *> pInt = new int(10);
// ...
delete pInt;
pInt = nullptr; // error!

In the above case you'll get:

error C2280: 
'not_null<int *> &not_null<int *>::operator =(nullptr_t)': 
attempting to reference a deleted function

I really advise not to use raw new/delete (my code is only for a demonstration!). Still, not_null gives here an strong hint: "don't mess with the pointer!". Such use case is also a topic of the ownership of such pointer. Since we have only a raw pointer (just wrapped with not_null), we can only observe it and not change the pointer itself. Of course, the code will compile when you only delete the pointer and don't clear it. But the consequences of such approach might be dangerous.

  • When you want to pass null to a function requiring a not null input parameter.

Violation of a contract!

void RunApp(gsl::not_null<App *> pApp) { }

RunApp(nullptr); // error!

You'd get the following:

function "gsl::not_null<T>::not_null(std::nullptr_t) [with T=App *]" cannot be referenced -- it is a deleted function

In other words, you cannot invoke such function, as there's no option to create such param from nullptr. With marking input arguments with not_null, you get a stronger guarantee. Much better than just a comment :)

  • Another reason to initialise when declaring a pointer variable.

While you can always initialize a pointer variable to nullptr, maybe it's better just to init it properly (with some real address/value/object) ?

Sometimes it will force you to rethink the code and move the variable to be declared later in the code.

int* pInt = nullptr;
// ...
pInt = ComputeIntPtr();
if (pInt) {
    // ...
}

Write:

// ...
not_null<int *> pInt = CompueInt();
// ...

You can play with the code below. Uncomment the code and see what errors you'll get...

Compile time
1
11
12
13
14
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// {...}
#include "gsl/gsl"
// {...}
void RunApp(gsl::not_null<App *> pApp)
{
pApp->Run();
pApp->Shutdown();
}
void DiagnoseApp(gsl::not_null<App *> pApp)
{
pApp->Diagnose();
}
int main()
{
// first case: deleting and marking as null:
{
gsl::not_null<App *> myApp = new App("Poker");
// we can delete it, but cannot assign null
delete myApp;
//myApp = nullptr;
}
// second case: breaking the contract
{
// cannot invoke such function, contract violation
//RunApp(nullptr);
}
// assigning a null on initilization
{
//gsl::not_null<App *> myApp = nullptr;
}
std::cout << "Finished...\n";
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Compile time is relatively easy. The compiler will reject the code, and we have just to redesign/fix it. But what about runtime?

Runtime

Unfortunately, the compiler cannot predict when a pointer becomes null. It might happen for various reasons. So how to get away with the if (pPtr) { } checks?

The expectations

For example:

void RunApp(not_null<App *> pApp);

App* pFakePtr = nullptr;
RunApp(pFakePtr);

By default we'll get (Under VS 2017, Windows):

Error

Under that condition the wrapper class can do the following:

  1. Terminate app
  2. Throw an exception
  3. Do nothing

How to control

You can control the behaviour using a proper #define.

See gsl_assert file: github.com/Microsoft/GSL/include/gsl/gsl_assert.

// 1. GSL_TERMINATE_ON_CONTRACT_VIOLATION: 
//       std::terminate will be called (default)
// 2. GSL_THROW_ON_CONTRACT_VIOLATION: 
//       a gsl::fail_fast exception will be thrown
// 3. GSL_UNENFORCED_ON_CONTRACT_VIOLATION: 
//       nothing happens

I probably prefer to use GSL_THROW_ON_CONTRACT_VIOLATION and that way we can use exceptions to check the null state.

Code rewrite

Let's look at the following example. When we have only a single pointer param it's simple anyway, but what if we have more:

So this (2 params):

void TestApp(App* pApp, TestParams* pParams)
{
    if (pApp && pParams)
    {
        // ...
    }
    else
        ReportError("null input params");
}

can become:

void TestApp(not_null<App *> pApp), not_null<TestParams *> pParams)
{
    // input pointers are valid
}

But now, all of the checks need to go to the caller:

// using
// #define GSL_THROW_ON_CONTRACT_VIOLATION

auto myApp = std::make_unique<App>("Poker");
auto myParams = std::make_unique<TestParams>();

try
{
    TestApp(myApp.get(), myParams.get());
    RunApp(myApp.get());
}
catch (std::exception& e)
{
    std::cout << e.what() << "\n";
    ReportError("null input params");
}

Is this better?

  • Might be, as we can handle nullptr pointer in only one place, shared for several 'child' functions.
  • We can move the checks up and up in the code, and in theory have only one test for null pointers.

You can play with the code below:

Runtime
1
11
12
13
14
15
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// {...}
#define GSL_THROW_ON_CONTRACT_VIOLATION
#include "gsl/gsl"
// {...}
struct TestParams { };
void ReportError(const std::string& str) { }
void RunApp(gsl::not_null<App *> pApp)
{
pApp->Run();
pApp->Shutdown();
}
void DiagnoseApp(gsl::not_null<App *> pApp)
{
pApp->Diagnose();
}
void TestAppCheck(App* pApp, TestParams* pParams)
{
if (pApp && pParams)
{
// ...
}
else
ReportError("null input params");
}
void TestApp(gsl::not_null<App *> pApp, gsl::not_null<TestParams *> pParams)
{
// input pointers are valid
}
int main()
{
auto myApp = std::make_unique<App>("Poker");
auto myParams = std::make_unique<TestParams>();
// older way:
TestAppCheck(myApp.get(), myParams.get());
// with catch:
try
{
// reset the pointer
myApp.reset(nullptr);
TestApp(myApp.get(), myParams.get());
RunApp(myApp.get());
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Issues

  • Smart pointers? The type is prepared to be used with smart pointers, but when I tried to use it, it looked strange. For now, I am not convinced. Although, the 'ownership' of a pointer and null state seems to be orthogonal.
  • Using with Spans
  • Converting constructors
  • Any difference between reference_wrapper? In C++ we have references that were designed not to hold null values, there's also a reference_wrapper class that are copyable and assignable. So cannot we just use ref wrapper instead of not_null?

Summary

Should we immediately use not_null everywhere in our code? The answer is not that obvious.

For sure, I am waiting to see such class in the Standard Library, not just in GSL. When it's included in STL, it would be perceived as a solid standardized helper to our code. I haven't seen any papers on that, however... maybe you know something about it?

Still, I believe it can help in many places. It won't do the magic on its own, but at least it forces us to rethink the design. Functions might become smaller (as they won't have to check for nulls), but on the other hand, the caller might require being updated.

It's definitely worth a try, so I plan to write more code with not_null.

Call to action:

  • Play with not_null for some time. Share your feedback.

This playground is adapted from my blog: Bartek's coding blog: How not_null can improve your code?

Visit the blog if you're looking for more good stuff about C++ :)

Open Source Your Knowledge: become a Contributor and help others learn. Create New Content