これだけ読めば全部がわかる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 newoperator 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 newoperator 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 deletestaticですので、当然ポリモーフィズムは働かない……と思いきや、b2の場合のみDerived2::operator deleteが呼ばれています。仮想関数を含んでいないため実行時型情報を持っていないb0Base0::operator deleteが呼ばれるのは当然のように思えますが、b1b2の違いは一体どこから来るのでしょうか?

 さて、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()であればエラーにはなりません。

C++実践プログラミング

C++実践プログラミング

Modern C++ Design―ジェネリック・プログラミングおよびデザイン・パターンを利用するための究極のテンプレート活用術 (C++ In‐Depth Series)

Modern C++ Design―ジェネリック・プログラミングおよびデザイン・パターンを利用するための究極のテンプレート活用術 (C++ In‐Depth Series)

  • 作者: アンドレイアレキサンドレスク,Andrei Alexandrescu,村上雅章
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2001/12
  • メディア: 単行本
  • 購入: 12人 クリック: 214回
  • この商品を含むブログ (101件) を見る