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

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

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!
}
 

This function crashed 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 crash.

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
}

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.

No comments:

Post a Comment

블로그를 이전합니다

뭐, 이런 작은 변방의 블로그에 관심있으신 분들은 아무도 없으시리라 생각합니다만...... (웃음) 블로그 플랫폼을 블로거에서 dev.to로 옮겼습니다. 새 URL은 아래와 같습니다: https://dev.to/teminian 새로운 거처에서 뵙겠습니...

Popular in Code{nested}