63904


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なのか、あるいはダメなのか知りたいです。


CC BY-SA 4.0
2020-03-16T08:47:59.543
63899
2020-03-17T19:04:21.387
2020-03-16T11:52:46.023
3060
37855
question
c++ c++11
例外を出しうるコンストラクタとnew式の挙動について


一般に、newを使わずに構築した場合、コンストラクタが失敗したことを知る方法はexceptionを使う以外にありません。

要するに、  
「コンストラクタで例外を出さないでね」というのは、  
「例外を捕捉するのが面倒なのでやめてね」  
ということではないかと思うのですが、どうでしょう。

捕捉できればメモリは何とでもできると思います。  
まぁ、自分もコンストラクタで例外が発生しないようにはしてます。


CC BY-SA 4.0
2020-03-16T09:40:32.103
63900
2020-03-16T09:40:32.103
3793
63899
answer


[`std::vector`コンストラクタ](https://ja.cppreference.com/w/cpp/container/vector/vector)は要素を確保するバージョンなどもあり、特に`std::bad_alloc`は割と発生しやすいと思います。（もちろんそうならないようコードを書くべきですが。）

で、C++言語としては、コンストラクターを実行する順序が厳密に定められていて、例外が発生した際にはコンストラクターの実行完了したオブジェクトに対しては、逆順にデストラクターを実行する仕様です。

[code]

    #include <iostream>
    
    struct a {
        a() { std::cout << "a .ctor." << std::endl; }
        ~a() { std::cout << "a .dtor." << std::endl; }
    };
    struct b {
        b() { std::cout << "b .ctor." << std::endl; }
        ~b() { std::cout << "b .dtor." << std::endl; }
    };
    struct c {
        c() {
            std::cout << "c .ctor." << std::endl;
            std::cout << "throw bad_alloc." << std::endl;
            throw std::bad_alloc{};
        }
        ~c() { std::cout << "c .dtor." << std::endl; }
    };
    struct d : a {
        b b;
        c c;    // ここのコンストラクターで例外
        d() { std::cout << "d .ctor." << std::endl; }
        ~d() { std::cout << "d .dtor." << std::endl; }
    };
    int main() {
        try {
            d d;
        }
        catch (std::bad_alloc&) {
            std::cout << "catch bad_alloc." << std::endl;
        }
        return 0;
    }
    
[/code]

このコード例であれば、親クラス`a`、メンバー変数`b`まではコンストラクターの実行が完了しているため、`b`→`a`の順でデストラクターが実行されます。

ですので、各コンストラクターが自身でメモリリークしないよう作られていれば、全体としては（およそ）メモリリークしないように言語設計されています。  
（それでも穴があるんでしたっけ？ 詳しい方お願いします。）


CC BY-SA 4.0
2020-03-16T10:10:46.030
63901
2020-03-16T10:10:46.030
4236
63899
answer


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

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

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


CC BY-SA 4.0
2020-03-16T14:09:53.777
63904
2020-03-16T14:09:53.777
4765
63899
answer


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

[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++はそれが出来るように設計されている。

です。


CC BY-SA 4.0
2020-03-17T19:04:21.387
63945
2020-03-17T19:04:21.387
3605
63899
answer