[C++]大域new/deleteの一部を置き換える

 なんかもう何してるのかわからなくなってきました。

 なんというか、グローバルなnewをオーバーロードしたいけど、一部分だけ元の処理を行いたい、っていうシチュエーションありませんか。ないですね。ないですけども。例えばメモリ管理は自前でやるけど、どっかから拾ってきたライブラリのメモリ管理はやりたくないです、みたいな時。ちょっと無理があるね。

 C++では、newは簡単にオーバーロードできます。placement newの文法を使うやつですね。ところが、deleteはplacement newみたいな使い方ができません。newとdeleteは対になってなきゃないはずなんだけど…うまいdeleteの呼び出し方がありません。

 グローバルのnewやdeleteをオーバーロードしてしまうと、あとから元のnewやdeleteを呼び出すことができません。したがって、一部分だけ挙動を変える、といったことができないのです。今回は、だいぶ無茶してnewとdeleteの一部分だけの挙動変更をやってみたいと思います。

はじめに

 だいぶ無茶するので、美しいコードを心がける皆様は回れ右したほうがいいかもしれません。

placement new/delete

 詳しくはググってください。と言いたいところですがこのあと使う話なので少しだけ説明を。

 placement new(配置new)は、あらかじめ確保されたメモリ領域に対してnew演算を行うための機能です。

inline void* operator new(size_t size) { return malloc(size); }

 newの実装は、大まかには上記のようになっています(実際にはちゃんと例外処理とかもしてあります)。確保すべきサイズが引数として渡ってくるので、そのサイズだけメモリを確保する、それだけです。ここで確保されたメモリに対して、各々のクラスのコンストラクタが呼ばれることになります。

 一方、placement newは以下の様な定義になっています。

inline void* operator new(size_t size, void *p) { return p; }

 すでに確保されたメモリ領域へのポインタがpに入るので、そのままそれを返します。使い方は少し特殊で、

void *p = malloc(128);
Test *t = new(p) Test();

 のような使い方をします。

 new演算子自体は引数をたくさん取ることができて、その内の一つがこのplacement newの構文になります。例えば、

inline void* operator new(size_t size, int a, int b, int c)
{
    return malloc(size);
}

 こんな感じの定義も可能で、使うときは

Test *t = new(10, 20, 30) Test();

 こうなります。賢い皆さんならお気づきのことでしょう。この引数の違いを用いることで、newはいくらでもオーバーロード出来るわけです。

 対してdeleteはどうなっているのかというと、いくらオーバーロードしても標準のdeleteしか呼ばれません。newを使用した時のような構文がサポートされていないからです。

delete(10, 20, 30) t; // コンパイルエラー!

 したがって、この厄介なdeleteさえ何とか出来れば、マクロを使ってnewとdeleteを置き換えることが可能になるわけです。

元のnewを利用する

 「元のnew」っていう表現はあまりよくないですね。「グローバルで定義したnewとは別のnew」とでも言いましょうか、今回はそういう意味合いで使っています。

 では早速newを書き換えましょう。

inline void* operator new(size_t size, int) { return malloc(size); }
#define new new(0)
// ... newを使った処理
#undef new

 newの方はこれで終わりです。newをplacement newで定義したものにすり替えるだけです。使い終わったらundefを使ってnewの定義を無効化します。

deleteの場合

 deleteは少し厄介です。先述の通り、deleteは引数の数でオーバーロードすることができません。したがって、別の方法に置き換えてやる必要があります。

 そもそもなんでこんな面倒な事をしているかというと、newやdeleteはマクロの形で呼び出されることが殆ど無いからです。これがマクロの形で呼び出されているのなら、単純にnewとdeleteを置き換えるだけでうまくいくのですが…外部のライブラリなどを使用しているとそうも行きません。

 じゃぁどうするかというと、なにか適当な別の演算子に置き換えることで対処する他ありません。しかし、単項演算子ではうまくオーバーロードできません。したがって二項演算子をオーバーロードします。幸いなことに、delete演算子は式中に組み込まれることがありませんし、単項演算子なので結合性も二項演算子より強力です(つまり、deleteに変数以外が指定されるとき、渡される式は括弧で囲まれることが多い、ということです)。

 実際にはこんな感じですり替えることができます。

struct __X
{
    static __X x;
    inline void operator+(void *p) { free(p); }
};

__X __X::x;
#define delete __X::x+
// ... deleteを使った処理
#undef delete

 だいぶアレですが。ただ実際にはこの定義はヘッダに書くことになると思うので、テンプレート化して__Xの実体化処理を遅らせるといいかもしれません。

template<int N>
struct __X
{
    static __X<N> x;
    inline void operator+(void *p) { free(p); }
};

template<int N>
__X<N> __X<N>::x;
#define delete __X<0>::x+
// ... deleteを使った処理
#undef delete

 ここで作った__Xをnewのオーバーロード決定用の引数にしてもいいかもしれません。

全体

 全体はこんな感じです。

template<int N>
struct __X
{
    static __X<N> x;
    inline void operator+(void *p) { free(p); }
};

template<int N>
__X<N> __X<N>::x;

inline void* operator new(size_t size, __X<0> &)
{
    return malloc(size);
}
#define delete __X<0>::x+
#define new new(__X<0>::x)
// ... 処理
#undef new
#undef delete

 これで一部分の置き換えはできてるんじゃないかなーと。

結論

 使い道は多分ない。

0 件のコメント :

コメントを投稿