0

I've previously worked in a setting where exceptions have been turned off and failed memory allocation means that we kill the program. Now working with exceptions I'm wondering about the precise semantics of the following:

class Foo {
  std::unique_ptr<Bar> x;
  std::unique_ptr<Bar> y;
 public:
  Foo(): x{new Bar}, y{new Bar} {}
};

My question is what happens when new Bar throws when y is being allocated? I would assume that the destructor of x is called so that the first allocation is cleaned up. How is the language guaranteeing this? Anyone know a quote from the standard that explains the precise semantics?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
del
  • 1,127
  • 10
  • 16
  • 2
    `y` is not "allocated" _per se_; its memory space is part of that of the `Foo`. The thing it points to is allocated by your `new Bar` expression. Just be careful with the terminology. I think you meant "initialised". – Asteroids With Wings Feb 29 '20 at 15:17

3 Answers3

4

Yes, all completely-constructed members will be destroyed. Your object will not be left in any sort of "half-alive" state. No memory leaks will occur.

[except.ctor]/3: If the initialization or destruction of an object other than by delegating constructor is terminated by an exception, the destructor is invoked for each of the object's direct subobjects and, for a complete object, virtual base class subobjects, whose initialization has completed ([dcl.init]) [..] The subobjects are destroyed in the reverse order of the completion of their construction. [..]

We can demonstrate this ourselves:

#include <memory>
#include <iostream>

struct Bar
{
    Bar(const char* name, bool doThrow = false) : m_name(name)
    {
        if (doThrow)
        {
            std::cout << name << ": Bar() throwing\n";
            throw 0;
        }

        std::cout << name << ": Bar()\n";
    }

    ~Bar() { std::cout << m_name << ": ~Bar()\n"; }

private:
    const char* m_name;
};

class Foo {
  std::unique_ptr<Bar> x;
  std::unique_ptr<Bar> y;
 public:
  Foo(): x{new Bar("A")}, y{new Bar("B", true)} {}
};

int main()
{
    try
    {
        Foo f;
    }
    catch (...) {}
}

// g++ -std=c++17 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
// A: Bar()
// B: Bar() throwing
// A: ~Bar()

(live demo)

This is, in fact, one of the major benefits of so-called "smart pointers": exception safety. Were x a raw pointer, you'd have leaked the thing it pointed to, because a raw pointer's destruction doesn't do anything. With exception safety you can have RAII; without it, good luck.

Asteroids With Wings
  • 17,071
  • 2
  • 21
  • 35
  • 1
    Also, the order of construction is the order of declaration of the members and strictly sequenced, so the members that would have their destructors called are all the ones declared before the one whose corresponding initializer is throwing. – walnut Feb 29 '20 at 15:18
3

If you are worried about the two new Bar expressions interleaving and throwing before the handles are initialized to hold what they are meant, the standard doesn't allow it.

First in [intro.execution]

12 A full-expression is

  • an init-declarator or a mem-initializer, including the constituent expressions of the initializer,

16 Every value computation and side effect associated with a full-expression is sequenced before every value computation and side effect associated with the next full-expression to be evaluated.

Without going too much into details, x{new Bar} and y{new Bar} in their entirety are both considered what standardese deems a "full-expression" (even though they are not expressions grammar-wise). The two paragraphs I quoted indicate that either the entire initialization of x (which includes new Bar) has to happen first, or the entire initialization of y has to happen first. We know from [class.base.init]

13.3 - Then, non-static data members are initialized in the order they were declared in the class definition (again regardless of the order of the mem-initializers).

So x is initialized in full, and then y. So even if new Bar throws while initializing y, x already owns the resource it's meant to hold. In which case, when the exception is thrown, the verbiage in [except.ctor] parageph 3 will apply to the fully constructed x, and it will be destructed, thus releasing the resource.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
0

If an exception is thrown during object construction the object was not constructed and you shouldn't make any assumptions about the state of any members. The object is simply not usable. Any members that were initialized prior to the exception being thrown will be destructed in reverse order of their construction.

https://eel.is/c++draft/basic.life#def:lifetime describes lifetime rules.

Jesper Juhl
  • 30,449
  • 3
  • 47
  • 70