今回は「[迷信] コンストラクタから例外を送出してはならない」の続編ともいうべき内容です。コンストラクタで失敗した場合の通知方法には例外を用いるのが最良ですが、いろいろなケースを考えると不安になる方も多いようです。今回取り上げるのは次のようなケースです。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class A { public: A() { throw 123; } }; int main() { A* p = 0; try { p = new A; } catch (...) { } delete p; return 0; } |
この例では、クラスAをnewで動的生成しようとしています。しかし、クラスAのコンストラクタは例外を送出しますので、動的生成に失敗し、catch節に分岐してしまいます。クラスAのコンストラクタは失敗しましたが、operator newには成功するため、割り付けたメモリを解放する機会が失われるというのです。
では本当にそんなことが起きるのか、上のコードに少し手を入れた次のコードを使って実験してみましょう。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#include <cstdio> #include <cstdlib> #include <new> void* operator new(std::size_t size) { std::puts("new"); return std::malloc(size); } void operator delete(void* ptr, std::size_t) noexcept { std::puts("delete"); std::free(ptr); } class A { public: A() { std::puts("A::A"); throw 123; } }; int main() { try { std::puts("try"); new A; } catch (...) { std::puts("catch"); } } |
この実験コードでは、newおよびdeleteを再定義して、呼び出されたかどうかが分かるように標準出力に”new”または”delete”という文字列を出力するようにしています。これをコンパイルして実際に動かしてみると、
0 1 2 3 4 5 6 |
try new A::A delete catch |
という結果が得られました。try からA::Aまでは説明の必要はないでしょう。そして、catchもどこで出力されたのか自明です。ではcatchの前の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&)があれば必ず定義しなければなりません。
これは、
0 1 2 |
new(std::nothrow) A; |
とした場合に、クラスAのコンストラクタが例外を送出すれば、operator delete(void* ptr, const std::nothrow_t&)が呼び出されるからです。もし対応するoperator deleteが定義されていなければ、そのときは本当にメモリリークが発生してしまいます。