operator newとoperator deleteのオーバーロード

アバター
tk-xleader
記事: 158
登録日時: 14年前
連絡を取る:

operator newとoperator deleteのオーバーロード

投稿記事 by tk-xleader » 9年前

std::listでnew・deleteをオーバーロード」で話題になっていたのでまとめ。

 C++では、operator newとoperator deleteもオーバーロードすることが出来ます。しかし、幾分か厄介な注意点が存在します。
 なお、以下の記述は非配列形式のnew/deleteを念頭に置いた記述ですが、配列形式であってもおそらく同じ注意点が当てはまるだろうと思います。

(1) あくまで、「operator new」「operator delete」のオーバーロードであるという点。

 C++において、

CODE:

auto X = new SomeType;
というコードがあった場合、次のような挙動を示します。

 operator new関数を呼び出す。→operator newの戻り値に対してオブジェクト構築(コンストラクタ呼び出し)。→オブジェクトのアドレスを返す。

 つまり、new演算子というのは、「領域の確保」「コンストラクタ呼び出しによるオブジェクト構築」の二段階の手順を踏んでいるわけで、operator new関数の役割は、そのうちの第一段階、すなわち「領域の確保」にあります。operator new関数が第一引数にsize_t型の引数を取る理由は、それによって確保しなければならないバッファの大きさを知るためです。処理系によりますが、デフォルトのoperator newは第一引数でmallocを呼び出すという実装が真っ先に想起される実装方法ですね。
 ちなみに、第二段階を書き換えることは基本的に出来ません。ということは、new演算子がオブジェクト構築以外に使われるということはありえないということになります。operator deleteも同じで、これも基本的には、"解体不要"な領域が引数として渡されることになります。つまり、渡された領域に対してデストラクタを呼び出すということはしてはいけません。

(2) delete演算子には引数を渡すことが出来ない。

 operator new関数で2つ以上の引数を取るものを定義した場合、以下のように呼び出すことが出来ます。

CODE:

new(Args...) SomeType
ところが、deleteには同じような呼び出し法がありません。したがって、原則として、プラスアルファを引数に取るoperator newに対応するoperator deleteを、delete演算子を通して明示的に呼び出すことは出来ません。このような場合、次のような方法を取ることになります。

CODE:

// SomeType* obj = new(X) SomeType;
obj->~SomeType() //明示的デストラクタ呼び出し
operator delete(obj,X); //明示的operator delete呼び出し
なお、delete X;は、Xの解体(デストラクタ呼び出し)。→operator delete(X);というコードとなるので、オーバーロードされたoperator new関数が内部で引数が一つのoperator newを呼び出しているというのであれば、他にoperator delete内で実行すべき処理(確保領域リストからの削除など)がなければdelete演算子を使ってオブジェクトを解体することも問題ありません。これの典型例がnothrow形式のnewです。std::nothrow_tを受け取るoperator newというのは、基本的にoperator newが発生させる例外、つまりstd::bad_allocを関数内で握りつぶしてnullptrを返す[anchor=x goto=note1](*a)[/anchor]というものなので、これを通常のoperator deleteで領域解放しても全く問題がないのです。

(3) operator newを定義した場合、対となるoperator deleteを必ず定義しなければならない。

 デフォルトのoperator newをユーザー定義した場合、対応するoperator deleteもユーザーが適切に定義しなければ問題がおこるのは当然ですが、(2)で述べたとおり、原則としてdelete演算子では引数つきnewに対応するoperator deleteを呼び出すことは出来ませんから、operator deleteのオーバーロードではなく、例えばdestoryとかいう名前で別にオブジェクト解体と領域解放を行う関数を用意すればいいのではないか(一つの関数にまとめた方がミスも減りますし…)と思うかもしれません。
 しかし、operator newを定義した場合、必ず対応するoperator deleteを定義しなければなりません。対応するoperator deleteとは、次のようなシグネチャを持つものをいいます。

CODE:

void* operator new(std::size_t size,A a,B b,..,Z z);
// operator new↑ ↓operator delete
void operator delete(void* p,A a,B b,..,Z z);
 or
void operator delete(void* p,std::size_t size,A a,B b,..,Z z);
 それでは、なぜoperator deleteを定義しなければならないのでしょうか。それは、(1)で述べましたが、new演算子というのは、基本的に「領域の確保」「オブジェクト構築」の二段階の処理を行います。では、第1段階の領域の確保には成功したが、第2段階のオブジェクトの構築に失敗した場合はどうなるのでしょうか。つまり、確保した領域にオブジェクトを構築するために呼んだコンストラクタが、不幸にも例外を投げてきた場合のことです。
 この場合、対応するoperator deleteが定義されていれば、コンパイラはそれを呼び出すように実行コードを生成します。つまり、確保された領域はoperator deleteによって解放されるということになります。中には、placement newに対応するoperator deleteのように、何もしないということもありますが(placement new自体が受け取ったアドレスをそのまま返すだけなので、operator deleteで何もしようがない)。もちろん、先ほど述べたdestroy関数を定義することも問題はありません。重要なのは、対応するoperator deleteが適切に定義されていないと、new演算子で構築しようとするオブジェクトのコンストラクタが例外を投げることが出来なくなってしまう(投げるとメモリリークが発生してしまう可能性が高い)ということです。

[anchor=note1]*a[/anchor] operator new関数では、関数が領域確保に失敗した場合、関数にnothrow指定をしている場合はnullptrを返し、それ以外の場合はstd::bad_allocでcatchできる例外を投げなければならないとされています。

YuO
記事: 947
登録日時: 14年前

Re: operator newとoperator deleteのオーバーロード

投稿記事 by YuO » 9年前

operator new/operator deleteではなく,operator allocate/operator deallocateとかであれば,op-new/op-deleteとnew-op/delete-opの混同問題はなかったとおもうのですが,今更ですしね……。
まぁ,Cとの互換性を壊すので,Keywordを増やしたくなかったのはあるでしょうが (C++11からはfinalやoverrideといった,C#におけるContextual Keywordsのようなものが存在しますが,C++03まではなかった)。

-- 2016-03-04T12:04+09:00 Modified: 脱字追加
最後に編集したユーザー YuO on 2016年3月04日(金) 12:04 [ 編集 2 回目 ]

アバター
tk-xleader
記事: 158
登録日時: 14年前
連絡を取る:

Re: operator newとoperator deleteのオーバーロード

投稿記事 by tk-xleader » 9年前

YuO さんが書きました:operator new/operator deleteではなく,operator allocate/operator deallocateとかであれば,op-new/op-deleteとnew-op/delete-opの混同問題はなかったとおもうのですが,今更ですしね……。
まぁ,Cとの互換性を壊すので,Keywordを増やしたくなかったのはあるでしょうが (C++11からはfinalやoverrideといった,C#におけるContextual Keywordsのようなものが存在しますが,C++03まではなかった)。
 確かに、命名を違えていれば、operator new/deleteのオーバーロードがnew/delete演算子の完全な書き換えとなると誤解されるという事態は防げていたのかもしれませんね。
 ただ、下手にキーワードを増やすのは避けたかったということに加えて、規格制定の際に、new/delete演算子に付随して呼び出される関数という認識があったということがこのような状況を作り出したのではないかとも僕は考えてます。