例外を出しうるコンストラクタとnew式の挙動について


Scott Mayers著『Effective Modern
C++』を読んでいるときに以下のようにコンストラクタ内でstd::make_uniqueを呼び出すコードに出くわしました。（原文とは少々変えています。）

[code]

    class A {
    private:
      struct B {
        //...
      };
      std::unique_ptr<B> ptr;
    public:
      A() : ptr(std::make_unique<B>()) {
      }
      ~A() = default;
      //...
    };
    
[/code]

ここで質問なのですが、std::make_uniqueはstd::bad_allocやstruct
Bのコンストラクタの例外を送出しうる関数なので、このクラスの利用側のコードでnewなどを呼び出したときにメモリリークが起こったりしないものでしょうか？  
例えば、

[code]

    void some_func() {
      A* a = nullptr;
      try {
        a = new A();   //例外！
      }
      catch(...) {
        delete a;
        return;
      }
      //...
      delete a;
    }
    
[/code]

などと書き、コンパイラがnew式において、

  1. operator newによるclass Aの領域確保
  2. コンストラクタA()
  3. 変数aへの代入(operator=)

の順で実行するコードを生成したとき、2.で例外が送出されるとaがnullptrのままで、deleteされずメモリリークになると思うのですが…  
コンストラクタは例外を送出しないように書くというイメージが強かったので、これは何でOKなのか、あるいはダメなのか知りたいです。




以下の記事によれば基本的には大丈夫な仕組みになっているようです。

[迷信] オブジェクトの動的生成に失敗するとメモリリークする  
<http://www.kijineko.co.jp/tech/superstitions/memory-leak-at-dynamic-creation-
fail.html>

> 実は、operator new が成功し、その後、生成しようとしたクラスのコンストラクタが例外を送出すると、自動的に operator delete
> が呼び出される仕掛けになっています。




メモリリークを起こすコードは簡単に書けます。

[code]

    #include <iostream>
    #include <memory>
    #include <exception>
    
    struct A {
        ~A() { std::cout << "A\n"; }
    };
    
    struct B {
        ~B() { std::cout << "B\n"; }
    };
    
    struct C {
        ~C() { std::cout << "C\n"; }
    };
    
    struct D {
        ~D() { std::cout << "D\n"; }
    };
    
    struct S {
        A a;
        std::unique_ptr<B> pb;
        C* pc = new C{};
    
        S(bool bThrow = false) {
            D* pd = new D{};
            pb = std::make_unique<B>();
    
            if (bThrow) {
                throw std::runtime_error("intentional");
            }
    
            delete pd;
        }
    
        ~S() {
            std::cout << "S\n";
            delete pc;
        }
    };
    
    void test1() {
        std::cout << "\n**** execption\n";
        try {
            S s1{ true };
        }
        catch (...) {
        }
    }
    
    int main() {
        test1();
        return 0;
    }
    
[/code]

上のコードを実行すると、以下のような結果が得られます。

[code]

    **** execption
    B
    A
    
[/code]

`S`のコンストラクタで例外を発生させると、まず`S`のデストラクタは呼ばれません。インスタンスが、まだ生成しきれていないので、デストラクタを呼ぶのは危険だからです。その代わり、`S`のために確保されたメモリ領域は解放されます。つまり`delete`は行われます。

次に、コンストラクタを呼び出す前に初期化されたメンバー変数のデストラクタは呼ばれます。`S::a`や`S::pb`がそうです。`S::pc`も、ある意味、後処理されてはいますが、生のポインタでデストラクタはないので、中身の解放まではしてくれません。

またコンストラクタ内で、例外を送出する前に確保したメモリも勝手には解放されません。

ただし、以上で紹介したリークは適切にコーディングすれば防ぐことができます。つまり

> コンストラクタ内で例外発生したときにメモリリークが起きないようにするのはプログラマの責任。C++はそれが出来るように設計されている。

です。

