[迷信] オブジェクトの動的生成に失敗するとメモリリークする

今回は、「[迷信] コンストラクタから例外を送出してはならない」の続編ともいうべき内容です。コンストラクタで失敗した場合の通知方法には例外を用いるのが最良ですが、いろいろなケースを考えると不安になる方も多いようです。今回取り上げるのは次のようなケースです。

class A
{
public:
    A()
    {
      throw 123;
    }
};

int main()
{
  A* p = 0;
  try
  {
    p = new A;
  }
  catch (...)
  {
  }
  delete p;
  return 0;
}

この例では、クラス Anew で動的生成しようとしています。しかし、クラス A のコンストラクタは例外を送出しますので、動的生成に失敗し、catch 節に分岐してしまいます。クラス A のコンストラクタは失敗しましたが、operator new には成功するため、割り付けたメモリを解放する機会が失われるというわけです。

では本当にそんなことが起きるのか、上のコードに少し手を入れた次のコードを使って実験してみましょう。

#include <cstdio>
#include <cstdlib>
#include <new>

void* operator new(std::size_t size) throw(std::bad_alloc)
{
  std::puts("new");
  return std::malloc(size);
}

void operator delete(void* ptr) throw()
{
  std::puts("delete");
  std::free(ptr);
}

class A
{
public:
    A()
    {
      std::puts("A::A");
      throw 123;
    }
};

int main()
{
  A* p = 0;
  try
  {
    std::puts("try");
    p = new A;
  }
  catch (...)
  {
    std::puts("catch");
  }
  delete p;
  return 0;
}

この実験コードでは、new および delete を再定義して、呼び出されたかどうかが分かるように標準出力に "new" または "delete" という文字列を出力するようにしています。これをコンパイルして実際に動かしてみると、

try
new
A::A
delete
catch
delete

という結果が得られました。try から A::A までは説明の必要はないでしょう。そして、catch もどこで出力されたのか自明です。最後の deletemain からリターンする直前に行ったものです。では、上から 4 行目、A::A の次の delete は一体なんでしょうか?

実は、operator new が成功し、その後、生成しようとしたクラスのコンストラクタが例外を送出すると、自動的に operator delete が呼び出される仕掛けになっています。実際に呼び出される operator delete は、メモリの割り付けに使用した operator new と対になるものです。

operator new は、普通は std::size_t 型の引数を一つだけ受け取りますが、必要に応じて追加の引数を受け取る operator new を多重定義することが可能です。その場合、その operator new に対応した operator delete も必ず多重定義する必要があります。operator delete(void* ptr, const std::nothrow_t&) など、明示的に呼び出す機会はまずないような operator delete も、それに対応する operator new(std::size_t size, const std::nothrow_t&) があれば必ず定義しなければなりません。

これは、

p = new(std::nothrow) A;

とした場合に、クラス A のコンストラクタが例外を送出すれば、operator delete(void* ptr, const std::nothrow_t&) が呼び出されるからです。もし対応する operator delete が定義されていなければ、そのときは本当にメモリリークが発生してしまいます。