Jonathan Boccara's blog

A Default Value to Dereference Null Pointers

Published May 14, 2021 - 0 Comments

With C++17, modern C++ has acquired a nullable object: std::optional. optional has a pretty rich interface, in particular when it comes to handling null optionals.

On the other hand, the oldest nullable type in C++, pointers, doesn’t have any helper to make the handling of its nullity more expressive.

Let’s see what we can do about it, to make our code using pointers, smart or raw, easier to read.

Handling std::nullopt

An optional<T> is an object that can have all the values that T can have, plus one: std::nullopt.

This allows to express the fact that a value can be “not set”, without resorting to sacrificing one possible value of T, such as 0, -1, or an empty string.

This allows in turn a function to manage errors by returning an optional. The semantics of this kind of interface is that the function should normally return a T, but it may fail to do so. In that case it returns nothing, or said differently in the language of optionals, it returns a std::nullopt:

std::optional<int> f()
{
    if (thereIsAnError) return std::nullopt;

    // happy path now, that returns an int
}

On the call site, the caller that gets an optional expects to find a value in it, unless it is a std::nullopt.

If the caller would like to access the value, it needs to check first if the optional returned by the function is not a std::nullopt. Otherwise, dereferncing a std::nullopt is undefined behaviour.

The most basic way to test for the nullity of the optional is to use its conversion to bool:

auto result = f();
if (result)
{
    std::cout << *result << '\n';
}
else
{
    std::cout << 42 << '\n'; // fallback value is 42
}

We can shorten this code by using the ternary operator:

auto result = f();
std::cout << result ? *result : 42 << '\n';

Except that in this particular case the code doesn’t compile, because of operator precedence. We need to add parentheses to clarify our meaning to the compiler:

auto result = f();
std::cout << (result ? *result : 42) << '\n';

This code is pretty clear, but there is a simpler way to express the simple idea of getting the value or falling back on 42.

To achieve that, optional provide the value_or member function, that allows to pack it into this:

std::cout << f().value_or(42) << '\n';

This has the same effect as the code above, but it is higher in terms of levels of abstraction, and more expressive.

Handling null pointers

Although they don’t have the same semantics at all, optional and pointers have one thing in common: they are both nullable.

So we would have expected a common interface when it comes to handling null objects. And indeed, we can test and deference pointers with the same syntax as optionals:

int* result = g();
if (result)
{
    std::cout << *result << '\n';
}
else
{
    std::cout << 42 << '\n';
}

Or, with the ternary operator:

int result = g();
std::cout << (result ? *result : 42) << '\n';

But we can’t write the nice one-liner for pointers:

std::cout << g().value_or(42) << '\n';

That’s a shame. So let’s write it ourselves!

Writing value_or with pointers

Until C++ has the uniform function call syntax that has been talked about for years (even decades), we can’t add a member function syntax to pointers, to get the exact same syntax as the one of optional.

But we can get pretty close with a free function, which we can write this way:

template<typename T, typename U>
decltype(auto) value_or(T* pointer, U&& defaultValue)
{
    return pointer ? *pointer : std::forward<U>(defaultValue);
}

We can then write our code dealing with null pointers like this:

std::cout << value_or(g(), 42) << '\n';

lvalues, rvalues? The devil is in the details

What should value_or return? In the above code, I’ve chosen to make it return decltype(auto). This makes the return type be exactly the same as the type on the return statement. Indeed, note that a simple auto would not have returned a reference, but rather a copy.

Now what is the type of the return statement? *pointer is an lvalue. The type returned by value_or depends on the type of defaultValue.

The general principle for the value category returned by the ternary operator is the following:

condition ? lvalue : lvalue // lvalue
condition ? lvalue : rvalue // rvalue
condition ? rvalue : lvalue // rvalue
condition ? rvalue : rvalue // rvalue

If defaultValue is an lvalue reference (which means that the argument it received was an lvalue), then std::forward<U>(defaultValue) is an lvalue, and so is the call expression ofvalue_or.

And if defaultValue is an rvalue reference (which means that the argument it received was an rvalue), then std::forward<U>(defaultValue) is an rvalue, and so is the call expression of value_or.

Do you find that value_or makes sense for pointers? How do you handle null pointer in your code?

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin