0

Error handling is a challenge in C++ constructors. There are several common approaches but all of them has obvious disadvantages. Throwing exceptions for example, may cause leak of the allocated resources earlier in the constructor, making it an error prone approach. Using a static init() method is another common solution, but it goes against the RAII principle.

Studying the subject I found this answer and blog suggesting the use of C++17 feature named std::optional<>, and I found it promising. However it seems that this kind of solution comes with an underlying problem - it triggers the destructor instantly when the user retrieved the object.

Here is a simple code example describing the problem, my code is based on the the above sources

class A
{
public:
    A(int myNum);
    ~A();
    static std::optional<A> make(int myNum);
    bool isBuf() { return _buf; };
private:
    char* _buf;
};

std::optional<A> A::make(int myNum)
{
    std::cout << "A::make()\n";
    if (myNum < 8)
        return {};
    return A(myNum);
}

A::A(int myNum)
{
    std::cout << "A()\n";
    _buf = new char[myNum];
}

A::~A()
{
    std::cout << "~A()\n";
    delete[]_buf;
}

int main()
{
    if (std::optional<A> a = A::make(42))
    {
        if (a->isBuf())
            std::cout << "OK\n";
        else
            std::cout << "NOT OK\n";

        std::cout << "if() finished\n";
    }
    std::cout << "main finished\n";
}

The output of this program will be:

A::make()
A()
~A()
OK
if() finished
~A()

followed with a runtime error (at least in Visual C++ environment) for attempting to delete a->_buf twice.

I used cout for the reader's convenience as I found this problem debugging a much complex code, but the problem is clear - the return statement in A::make() constructs the objects, but since it is the end of the A::make() scope - the destructor is invoked. The user sure his object is initialized (notice how we got an "OK" message) while in reality it was destroyed, and when we step out of the if() scope in main, a->~A() is invoked once again.

So, am I doing this wrong? The use of std::optional for error handling in constructors is common, or so I've been told. Thanks in advance

Z E Nir
  • 332
  • 1
  • 2
  • 15
  • Looks to me like a violation of [the rule of 3/5/0](https://en.cppreference.com/w/cpp/language/rule_of_three). You copy a `A`, and both instances try to delete the same pointer. Edit : Looks like changing `return A(myNum);` to `return {myNum};` hides the problem, so it is likely a rule of 3/5/0 error. – François Andrieux Dec 05 '20 at 18:20
  • You've been told wrong. Use of `std::optional` for "error handling in constructors" is not common. There's nothing inherently more complicated about error handling in constructors than in any other parts of C++ programs. As long as fatal errors result in correctly thrown exceptions C++ will automatically take care of all the low-level details, such as destroying already-constructed class members (in reverse order), etc... Use the right tool for the right job. A construction failure is simply reported by throwing an exception. Do you know what exceptions are and how to use them? – Sam Varshavchik Dec 05 '20 at 18:20
  • I know what are exceptions allright... Im just trying to learn new things by using them wrong lol. after a whole day spent on this optional nonsense i just used exceptions and moved on. still, I decided to ask, and here Im learning about the rule of 3/5/0... thanks you two – Z E Nir Dec 05 '20 at 18:30

1 Answers1

1

Your class violates the rule of 3/5.

Instrument the copy constructor and simplify main to get this:

#include <optional>
#include <iostream>

class A
{
public:
    A(int myNum);
    ~A();
    A(const A& other){
        std::cout << "COPY!\n";
    }
    static std::optional<A> make(int myNum);
    bool isBuf() { return _buf; };
private:
    char* _buf = nullptr;
};

std::optional<A> A::make(int myNum)
{
    std::cout << "A::make()\n";
    if (myNum < 8)
        return {};
    return A(myNum);
}

A::A(int myNum)
{
    std::cout << "A()\n";
    _buf = new char[myNum];
}

A::~A()
{
    std::cout << "~A()\n";
    delete[]_buf;
}

int main()
{
    
    std::optional<A> a = A::make(42);
    std::cout << "main finished\n";
}

Output is:

A::make()
A()
COPY!
~A()
main finished
~A()

When you call A::make() the local A(myNum) is copied to the retunred optional and its destructor is called afterwards. You'd have the same issue without std::optional (eg by returning an A by value).

The copy constructor I added does not copy anything, but the compiler generated one does make a shallow copy of the char* _buf; member. As you do not properly deep copy the buffer it gets deleted twice which results in the runtime error.

Use a std::vector for the rule of 0, or properly implement the rule of 3/5. Your code invokes undefined behavior.


PS Not directly related to the problem, but you should initialize members instead of assigning to them in the constructors body. Change:

A::A(int myNum)
{
    std::cout << "A()\n";
    _buf = new char[myNum];
}

to

A::A(int myNum) : _buf( new char[myNum])
{
    std::cout << "A()\n";
}

or better yet, use a std::vector as mentioned above.


PPS:

Throwing exceptions for example, may cause leak of the allocated resources earlier in the constructor, making it an error prone approach.

No, throwing from a constructor is common and has no problem when you don't manage memory via raw pointers. Both using a std::vector or a smart pointer would help to make your constructor excpetion safe.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • forgot to set `_buf` to `nullptr`? what r u `delete`ing in case of copy? – Red.Wave Dec 05 '20 at 18:32
  • @Red.Wave ? "forgot to set _buf to nullptr?" what do you mean? I do initialize `_buf` to `nullptr` to have it explicitly set to something in the copy (note that my copy constructor is only for illustration, but not actually copying anything). In my copy nothing is deleted, because `_buf` is `nullptr` – 463035818_is_not_an_ai Dec 05 '20 at 18:34
  • About your PPS, I did mean using of raw pointers when I claimed throwing exception can cause leaks. I know it is not a good practice in C++ to use raw pointers, but I cannot ignore it is possibe... – Z E Nir Dec 05 '20 at 19:21
  • 1
    @ZENir but if you want to use raw owning pointers then taht is the problem ;) It is nothing that one cannot avoid – 463035818_is_not_an_ai Dec 05 '20 at 19:22