0. はじめに
Effective C++ 第3版 52項 「プレースメントnewの定義を書いたらプレースメントdeleteも書こう」に関して, 実験を通して理解を深めます.
検証環境はつぎのとおりです.
構成要素 | 種類 |
---|---|
OS | macOS Catalina 10.15.2 |
clang | 11.0.3 |
また, ここで__プレースメントnew__とは__「size_t 以外に引数を受け取る operator new」__のことです.
1. コンストラクタで例外が発生したら, メモリ解放漏れになる
まず, つぎのとおり__'文字列を取るプレースメントnew'__を定義してみます.
#include <iostream>
using namespace std;
class A{
public:
int a;
A(int a){
this->a = a;
}
~A(){
cout << "dctor" << endl;
}
// 文字列を取るプレースメントnew
void* operator new(size_t size, const string& msg) {
cout << "operator new! " << msg << endl;
void* p = ::operator new(sizeof(A));
return p;
}
};
int main(){
try{
A* ap = new("Hello")A(5);
cout << ap->a << endl;
delete ap;
}catch(exception& e){
cout << e.what() << endl;
}
}
この実行結果は, つぎのとおりになります.
operator new! Hello
5
dctor
つぎに, コンストラクタで例外を発生させてみます.
A(int a){
this->a = a;
throw exception();// 例外発生!!!
}
この実行結果は, つぎのとおりになります.
operator new! Hello
std::exception
なるほど, 確かにデストラクタは呼ばれていません.
なお, このケースでは呼び出し元(main)で delete することはできません.
2. 対応するプレースメントdeleteを定義すれば, メモリを開放できることの確認
つぎのとおり, __プレースメントdelete__を定義してみます.
// プレースメントdeleteが通常のdeleteを隠してしまうので, 通常版も定義しておく
void operator delete(void* p){
::operator delete(p);
}
// プレースメントdelete
void operator delete(void* p, const string& msg){
cout << "operator delete!" << endl;
delete static_cast<A*>(p);
}
この実行結果は, つぎのとおりになります.
operator new! Hello
operator delete!
dctor
std::exception
なるほど, 確かに__「C++ランタイムが対応するプレースメントdeleteを探して実行してくれる」__ということは確認できました. なお, ここで注意したいのは実行するのはプレースメントの __operator delete__であって, __delete演算子__を実行するわけではないので, プレースメントdelete の実装でデストラクタが呼ばれるように保証しないといけないことです.
3. 補足
ん?あれ?
自分で「プレースメントdelete の実装でデストラクタが呼ばれるように保証しないといけないことです」とか書いて引っかかりました. それは次のケースでは通常の operator delete が呼ばれますが, その中でデストラクタを呼ぶような実装には当然なっていないからです.
#include <iostream>
using namespace std;
class A{
public:
int a;
A(int a){
this->a = a;
throw exception();
}
~A(){
cout << "dctor" << endl;
}
};
int main(){
try{
A* ap = new A(5);
cout << ap->a << endl;
delete ap;
}catch(exception& e){
cout << e.what() << endl;
}
}
この路線で考えると, 「ごく普通のクラス定義のケースでもデストラクタが呼ばれるように毎回 operator delete を再定義しろ」ということになってしまいます. これはさすがにないな〜とおもいました.
最終的に__「コンストラクタの中できちんとお掃除してから, 呼び出し元でキャッチすべき意図的な例外を送出する実装・I/Fがある場合にきちんと動作させる (そのオブジェクト自体を開放する) 方法」__ということだと理解しました. なので, デストラクタは呼ばれなくてもいいと思います.
上のことを逆に解釈すると, 「コンストラクタから送出された例外を無闇にキャッチして続行するとメモリ漏れになる」とも言えると思います.