これだけ読めば全部がわかるC++のoperator new/deleteオーバーロードの注意点
……とか言いながら、全然「だけ」に留まらない分量になってるのは仕様(C++の)。
なお本記事は、operator new
/delete
の概念はある程度理解し、その気になればメモリリークしない程度のoperator new
/delete
を実装できるC++erに向けて書かれたものです。operator new
/delete
の中身(new_handlerの呼び出しとか)についての説明は行ないませんので悪しからず。
本記事で「規格書」と言った場合、正式なC++11の規格書ではなくhttp://d.hatena.ne.jp/heisseswasser/20121225/1356462583:N3337を指しています。
また、特に明記のない限り、本記事でoperator new
、operator delete
と言った場合、配列版のoperator new[ ]
、operator delete[ ]
についても同様であるものとします。
非プレースメントnew/deleteと、プレースメントnew/deleteについて
まずは用語の定義みたいなものを。
通常の、new Hoge();
の形式で呼び出すoperator new
をプレースメントnew、それに対応して呼び出されるべきoperator delete
をプレースメントdeleteと呼びます。そうでないものは非プレースメント。
本来placementというのは配置という意味なので、メモリプール等の実装時に使う『特定のアドレスにオブジェクトを配置する処理』を前提にした用語だったのでしょうが、実際には第二引数以降にはポインタに限らずどんな型でも渡せます。可変長引数テンプレートでごそっと渡すこともできます(XCodeでは可変長にするとclangが落ちる場合がありましたが)。
クラス定義のoperator new/deleteは必ずstaticになる
ウェブ上のサンプルコードなどを見ると、これらのオーバーロードに対してstatic
がついていたりついていなかったりします。一体どちらが正しいのでしょうか?
答えは『どちらでもよい』です(プレースメント、非プレースメントにかかわらず)。
というのもC++の規格書には、これらの定義は(仮にstatic
宣言をしなかったとしても)常にstatic
である(>>even if not explicitly declared static<<)とあるためです。もちろん、virtual
にはできません。
もっとも、いくら省略が可能といっても、static
なものは明示的にstatic
宣言しておく方が当然好ましいはずですので、本記事ではそのようにしておきます。
operator new/deleteは、メモリ確保だけを行なう
new
構文やdelete
構文がコンストラクタやデストラクタを呼ぶので混同されがちですが、operator new
/delete
(もちろん配列版も)は、コンストラクタやデストラクタの呼び出しを『行なってはいけません』。
不幸にもC++には、指定したアドレスに対してコンストラクタやデストラクタを明示的に呼び出す方法が存在するため、オーバーロードしたoperator new
/delete
内でコンストラクタやデストラクタを呼ぶ、次のような間違ったコードを書いてしまいがちです。
struct IllegalAllocation { IllegalAllocation() { std::cout << "constructor" << std::endl; } ~IllegalAllocation() { std::cout << "destructor" << std::endl; } static void *operator new(std::size_t s) { return ::new (malloc(s)) IllegalAllocation; } static void operator delete(void *p) { ((IllegalAllocation*)p)->~IllegalAllocation(); ::operator delete(p, p); free(p); } };
上記のクラスを一度だけnew
すると、コンストラクタとデストラクタが二回ずつ呼ばれます。もちろん、コンストラクタで確保したリソースはもう一つのコンストラクタで上書きされてリソースリークし、デストラクタは同じリソースを二重に解放します。言うまでもなく危険なコードです。
operator newとoperator deleteの対応関係
たとえ何らかの理由によりdelete
呼び出しをしない時でも、operator new
を定義したら、必ず対になるoperator delete
を宣言しておきましょう。対になるoperator delete
とは、第二引数以降がoperator new
の第二引数以降と一致するようなもののことです。
これをしなければならないのは、メモリ確保には成功したけれどクラスのコンストラクタで例外が発生した時に、対になるoperator delete
が呼ばれるためです(もしもそのようなoperator delete
が存在しなければ、解放処理自体が行なわれません!)。
確保しっ放しでよい場合でも、何もする必要がないことを明示するため、空のoperator delete
を定義しておくのが好ましいでしょう。
対となるoperator new
とoperator delete
の引数型が異なってくる唯一の例外は、次のような場合です。
struct TwoParameteredDeallocation { void *operator new(std::size_t s); void operator delete(void *p, std::size_t s); };
規格書には、次のようにあります。
If a class T has a member deallocation function named operator delete with exactly one parameter, then that function is a usual (non-placement) deallocation function. If class T does not declare such an operator delete but does declare a member deallocation function named operator delete with exactly two parameters, the second of which has type std::size_t, then this function is a usual deallocation function.
(クラスが唯一の引数を持つoperator delete
を持っている場合、それは非プレースメントdelete
である。クラスがそのようなoperator delete
を持っておらず、ただ二つの引数を持ち、二つ目の引数の型がstd::size_t
であるような場合、それが非プレースメントdelete
となる)
...
A template instance is never a usual deallocation function, regardless of its signature.
(テンプレートを実体化したoperator delete
は、それがどんなシグネチャーを持っていたとしても[※つまり、テンプレートを実体化結果として引数がvoid*
のみやvoid*, std::size_t
になったとしても]、決してプレースメントdelete
にはなり得ない)
さらに、別の場所には次のようにも書かれています。
If the lookup finds the two-parameter form of a usual deallocation function and that function, considered as a placement deallocation function, would have been selected as a match for the allocation function, the program is ill-formed.
(もし、2パラメータ版の非プレースメントdelete
が存在し、それがプレースメントdelete
としてnew
の対になる場合[※すなわちoperator delete(std::size_t)
が存在しないのにoperator delete(std::size_t, std::size_t)
が存在し、operator new(std::size_t, std::size_t)
が使用された場合]、プログラムは構文エラーである。
つまり、次のような場合には、operator delete(void*, std::size_t)
はoperator new(std::size_t)
と対応する関数としては見なされない、ということです。
struct WellFormed { static void *operator new(std::size_t); // 非プレースメントnew static void *operator new(std::size_t, std::size_t); // プレースメントnew static void operator delete(void*); // 非プレースメントdelete static void operator delete(void*, std::size_t); // 非プレースメントnew }; struct AnotherWellFormed { static void *operator new(std::size_t); // 非プレースメントnew static void *operator new(std::size_t, const WellFormed&); // プレースメントnew(対応するoperator deleteは存在しないので、解放関数は呼ばれない) static void operator delete(void*); // 非プレースメントdelete static void operator delete(void*, std::size_t); // プレースメントdelete(対応するoperator newは存在しないが、構文エラーではない) }; struct IllFormed { static void *operator new(std::size_t); // 非プレースメントnew static void *operator new(std::size_t, std::size_t); // プレースメントnew static void operator delete(void*, std::size_t); // 「非」プレースメントdelete }; struct AnotherIllFormed { static void *operator new(std::size_t, std::size_t); // プレースメントnew static void operator delete(void*, std::size_t); // 「非」プレースメントdelete(対応するoperator newは存在しないが、それ自体は構文エラーではない) }; int main() { // それぞれ、deleteは省略 auto pwf1 = new WellFormed(); // OK auto pwf2 = new (1) WellFormed(); // OK auto pawf1 = new AnotherWellFormed(); // OK //auto pawf2 = new (1) AnotherWellFormed(); // 対応するプレースメントnewがないので当然エラー auto pawf3 = new (*pwf1) AnotherWellFormed(); // OK auto pif1 = new IllFormed(); // operator new(std::size_t, std::size_t)さえ使用しなければOK //auto piwf2 = new (1) IllFormed(); // 構文エラー! //auto paif1 = new AnotherIllFormed(); // 非プレースメントnewが存在しないので当然エラー //auto paiwf2 = new (1) AnotherIllFormed(); // 構文エラー! return 0; }
なお、C++11の範囲ではoperator delete(void*, std::size_t)
が定義できるのはクラス内のみですが、C++14ではグローバルでもoperator delete(void*, std::size_t)
が定義できるようにするという提案がなされています。その場合はどうやら、グローバルではサイズつき版が優先される模様です。
operator deleteのポリモーフィズム
次のコードを実行してみて下さい。
struct Base0 { static void *operator new(std::size_t s) { std::cout << "Base0::operator new" << std::endl; return malloc(s); } static void operator delete(void *p) { std::cout << "Base0::operator delete" << std::endl; free(p); } }; struct Derived0 : public Base0 { static void *operator new(std::size_t s) { std::cout << "Derived0::operator new" << std::endl; return malloc(s); } static void operator delete(void *p) { std::cout << "Derived0::operator delete" << std::endl; free(p); } }; struct Base1 { virtual void x() {} static void *operator new(std::size_t s) { std::cout << "Base1::operator new" << std::endl; return malloc(s); } static void operator delete(void *p) { std::cout << "Base1::operator delete" << std::endl; free(p); } }; struct Derived1 : public Base1 { virtual void x() override {} static void *operator new(std::size_t s) { std::cout << "Derived1::operator new" << std::endl; return malloc(s); } static void operator delete(void *p) { std::cout << "Derived1::operator delete" << std::endl; free(p); } }; struct Base2 { virtual ~Base2() {} static void *operator new(std::size_t s) { std::cout << "Base2::operator new" << std::endl; return malloc(s); } static void operator delete(void *p) { std::cout << "Base2::operator delete" << std::endl; free(p); } }; struct Derived2 : public Base2 { virtual ~Derived2() override {} static void *operator new(std::size_t s) { std::cout << "Derived2::operator new" << std::endl; return malloc(s); } static void operator delete(void *p) { std::cout << "Derived2::operator delete" << std::endl; free(p); } }; int main() { Base0 *b0 = new Derived0; // Derived0::operator new delete b0; // (1) Base0::operator delete Base1 *b1 = new Derived1; // Derived1::operator new delete b1; // (2) Base1::operator delete Base2 *b2 = new Derived2; // Derived2::operator new delete b2; // (3) Derived2::operator delete return 0; }
operator delete
はstatic
ですので、当然ポリモーフィズムは働かない……と思いきや、b2
の場合のみDerived2::operator delete
が呼ばれています。仮想関数を含んでいないため実行時型情報を持っていないb0
でBase0::operator delete
が呼ばれるのは当然のように思えますが、b1
とb2
の違いは一体どこから来るのでしょうか?
さて、C++の規格書を紐解いてみると、次のようになっています。
In the first alternative (delete object), if the static type of the object to be deleted is different from its dynamic type, the static type shall be a base class of the dynamic type of the object to be deleted and the static type shall have a virtual destructor or the behavior is undefined.
(一番目の使い方(delete ptr;
構文)では、オブジェクトの静的型[※ポインタが指す型のこと]が動的型[※実際のオブジェクト型のこと]と異なる場合、静的型は動的型の基底クラスでなければならず、さらに静的型は仮想デストラクタを持たなければならない。そうでなければ、deleteの
動作は未定義である)
つまり、実はC++ではb1
のみならずb0
も未定義の動作であり、C++の規格に則っているのはb2
だけだ、ということです。つまり、継承される可能性のあるクラスは必ずデストラクタをvirtual
にしないと、インスタンスが保持するリソースどころかインスタンスそのものがメモリリークする可能性すらあるわけです。
このoperator delete
は仮想関数によるオーバーロードではないため、静的型と動的型のうち片方が/code>operator delete(void*)、もう片方がoperator delete(void*, std::size_t)
になっている場合でも、シグネチャーの違いを無視して動的型のoperator delete
が呼び出されます。
また規格書によると、delete[ ]
の場合は次のようになっています。
In the second alternative (delete array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.
(二番目の使い方(delete[ ] ptr;
構文)では、オブジェクトの静的型と動的型が異なる場合、動作は未定義である)
配列へのポインタを基底クラスのポインタに変換したら、それを配列として扱うのはやめろって事ですね(まあ、基底クラスにoperator[ ]
を使ったら当然未定義になるので、当たり前っちゃ当たり前ですが)。
クラス定義のoperator new/deleteを無視する方法
実のところ、クラスで定義したoperator new
/delete
は使わないことができます。
struct Hoge { static void *operator new(size_t s) { std::cout << "Hoge::operator new" << std::endl; return malloc(s); } static void operator delete(void *p) { std::cout << "Hoge::operator delete" << std::endl; free(p); } }; int main() { Hoge *p0 = new Hoge(); // Hoge::operator new delete p0; // Hoge::operator delete Hoge *p1 = ::new Hoge(); // global operator new ::delete p1; // global operator delete return 0; }
なので、void *operator new(std::size_t) = delete;
となっていても、::
さえあれば簡単にnew
できます。もちろん、グローバルなoperator new(std::size_t, std::size_t)
が宣言されているのなら、「operator newとoperator deleteの対応関係」でエラーとして挙げたようなコードも::new (1) IllFormed()
であればエラーにはなりません。