Help us understand the problem. What is going on with this article?

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

0. はじめに

Effective C++ 第3版 52項 「プレースメントnewの定義を書いたらプレースメントdeleteも書こう」に関して, 実験を通して理解を深めます.

検証環境はつぎのとおりです.

構成要素 種類
OS macOS Catalina 10.15.2
gcc 4.2.1

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした