DEV Community

Robert Teminian
Robert Teminian

Posted on

std::move = when std::optional should be launched

A post from my old blog:
https://codenested.blogspot.com/2023/01/stdmove-when-stdoptional-should-be.html


Recently I had a chance to take a look at Rust. When returning from a function Rust uses std::option to return either class A in success or class B in exception.

And I found out something similar in C++, namely std::optional. Most of C++ users argued that "why use std::optional when we can fully make use of null pointers?" According to C++ Committee, it was due to minimize human errors. Then we can ask one thing: what kind of errors, then?

Let's take a look at the code below:

struct Insider {
    /* whatever great data structure */
};

struct anti_memory_leak {
    Insider *insider=nullptr;
    ~anti_memory_leak() {
        if(insider) delete insider;
    }
};
Enter fullscreen mode Exit fullscreen mode

There's nothing special in this code. Since the class Insider can be used optionally, it can be allocated to heap. When anti_memory_leak is removed from memory, Insider object will be also removed in destructor so we have means for memory leak. We proved that we can do it without std::optional.

...... Did we?

Then let's investigate the code below:

void doSomethingGreat()
{
    anti_memory_leak object1;
    object1.insider=new Insider();

    std::vector<anti_memory_leak> vector1;
    vector1.push_back(std::move(object1));
    vector1.back().insider->value1=20; // CRASH!
}
Enter fullscreen mode Exit fullscreen mode

This function crashes in the last line. Why? The reason is in the one line above. When you call vector1.push_back(), even though you use std::move() object1 is destructed and recreated. And when destructing, the destructor of anti_memory_leak is called, and it surely remove insider from the heap. In other words, vector1.back().insider becomes a dangling pointer. It's kind of unfortunate, the result is the same if you use emplace_back() instead of push_back(). Anyway the application crashes.

Now is the time std::optional should be used. If you declare an object with std::optional, the memory is initialized without allocating that optional object, and it is initialized when the optional object is explicitly created. Of course there's a small overhead, but say, it's also same for other similar(?) classes like std::shared_ptr. We have raw pointers, but to manage our precious heap more safely, we can automate some of the management so that we can solve potential incidents(including both memory leak and dangling pointer) more easily.

If we refactor the code above using std::optional, it will be like this:

struct Insider {
    int value1;
    /* whatever great data structure */
};

struct anti_memory_leak {
    std::optional<Insider> insider;
    ~anti_memory_leak() {
        if(insider) delete insider;
    }
};

void doSomethingGreat()
{
    anti_memory_leak object1;
    object1.insider=Insider();

    std::vector<anti_memory_leak> vector1;
    vector1.push_back(std::move(object1));
    vector1.back().insider.value1=20; // OK
}
Enter fullscreen mode Exit fullscreen mode

If the flow of the code is simple it won't be a big problem. However, if the flow becomes anyway compilcated(e.g. multithreading), there should be chances to free memory or miss the chance when we have to, regardless of my intention. Let's think of std::optional as some kind of insurance policy; though we all agree that insurance fee is somewhat "waste of money"(lol), but we spend money to prepare for the worst? I think it's the same for std::optional.

Top comments (4)

Collapse
 
pauljlucas profile image
Paul J. Lucas

You never need to check a pointer for null before delete. The delete of a null pointer is guaranteed to do nothing.

When you wrap std::optional around an object, you no longer have a pointer, so saying delete insider won't even compile!

Collapse
 
teminian profile image
Robert Teminian

Oops. lol

Found this and fully understood the situation.
en.cppreference.com/w/cpp/utility/...

Thank you for pointing out!

Collapse
 
pgradot profile image
Pierre Gradot

std::optional is not meant to replace pointers. Exactly because pointers can be null. When to express the fact that the object may be missing, it's easy to use a pointer that can be null.

std::optional is really useful when you don't have pointer and when to express the fact that the object may be missing.

In the case you are presenting (apart from the good points raised by @pauljlucas ), I think you are missing something important: using std::optional is probably not the solution. You should probably write a move constructor. Indeed, you are breaking The rule of three/five/zero.

Adding these lines would do the trick:


    anti_memory_leak() = default;

    anti_memory_leak(anti_memory_leak&& other) noexcept {
        std::swap(insider, other.insider);
    }
Enter fullscreen mode Exit fullscreen mode

I assume that you have created this class only for the demo here, and that you should use std::unique_ptr instead :) If you're on fire, you get have a look at the implementation github.com/gcc-mirror/gcc/blob/mas... and you will see how the move constructor is implemented (line 178).

Furthermore, when I execute your code, I get a double free error:

free(): double free detected in tcache 2
Program terminated with signal: SIGSEGV
Enter fullscreen mode Exit fullscreen mode

Indeed, vector1.back().insider->value1=20; isn't actually guaranteed to crash. It's "just" an undefined behavior ;)

Collapse
 
teminian profile image
Robert Teminian

Oops. lol (2)

Yes you're right. Totally agreed. Nowadays I'm practicing Rust and I think I finally understand the raison d'etre of std::optional in C++ (I see you, Some() and None() in Rust).

Yet, I didn't think of implementing move constructor myself. Next time I had better consider yet another card in my hand.