std::optional – removing confusion around pointer types

std::optional is a useful C++17 feature for knowing when a value is set. There’s been some debate however as to its usefulness for 2 forms.

1. std::optional<bool>
2. std::optional<T*>

Both forms are criticized for being confusing to readers. Considering that this post stems from having to explain recently what it means when std::optional wraps a pointer, I’m inclined to agree. The code below shows where the confusion lies:

std::optional<SomeDataType*> myval;
...
myval = nullptr;

if (myval)
// ?? Is this true or false?

In the highlightedline of the if statement, does will the value evaluate as true or false? It, in fact, does evaluate as true, and the value of operator bool is true. This is where the confusion is though. Some developers see std::optional as a simplified version of a wayt to wrap a pointer, similar to a smartptr. But it is quite different. In this case having set the value, even to nullptr, means the variable is “set”, so to speak. It may not sound very useful, but there are some cases, were knowing if a value is set, where nullptr is a valid value, is useful. The example below of a fast cache that shows that usefulness:


struct SomeDataType
{
    // may have lots of data
    int myint;
};
const std::size_t max_idx = 100;
std::array<std::optional<SomeDataType*>, max_idx> fast_cache;

SomeDataType* getRefSlow( unsigned idx )
{
    // would do an expensive call
    // If value exists, return a ptr to it.
    // 
    // if no value exists, return nullptr.
    return nullptr;
}

std::optional<SomeDataType*> getFastCache(const size_t idx)
{
    std::optional<SomeDataType*> my_data;
    if ( !fast_cache[idx] )
    {
        std::cout << "slow call" << std::endl;
        my_data = std::optional<SomeDataType*>(getRefSlow(idx));
        fast_cache[idx] = my_data;
    }
    else
        std::cout << "fast call" << std::endl;
        my_data = fast_cache[idx];

    return my_data;
}

int main()
{

    size_t idx = 12;
    std::optional<SomeDataType*> my_data = getFastCache(idx);
    // call the same 
    my_data = getFastCache(idx);

}

outputs:

slow call
fast call

Do note, that you’ll want to be careful if the pointer can be deleted somewhere else from under you, as it can you leave with you a dngling pointer. Worse yet, the std::optional variable will still show the data as being set. Again, this std::optional isn’t similar to a smartptr.

    std::optional<SomeDataType*> my_dangler = new SomeDataType();

    // usually this would not be this clear...
    delete *my_dangler;

    std::cout << (bool)my_dangler << std::endl;

outputs:

1

We could, of course get around this by using a tuple< SomeDataType*, bool> to track if the value is set or not. However, std::optional is much easier and cleaner way. I think once developers get used to std::optional, it will be more common and provide for cleaner, and more elegant code.

Leave a Reply

Your email address will not be published. Required fields are marked *