LoginSignup
1
4

More than 1 year has passed since last update.

'Placement new' について実験してみた

Last updated at Posted at 2020-01-13

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がある場合にきちんと動作させる (そのオブジェクト自体を開放する) 方法」ということだと理解しました. なので, デストラクタは呼ばれなくてもいいと思います.

上のことを逆に解釈すると, 「コンストラクタから送出された例外を無闇にキャッチして続行するとメモリ漏れになる」とも言えると思います.

1
4
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4