EC++ #50, #51, #52 プレースメント new/delete

Effective C++ の 50, 51, 52項の内容です.本的には第8章をまるごと new/delete の話にさいていましたが,これを機会にちょっと new/delete の定義を見てみることにしました.

new/delete とは?

下記,自分の環境にあった new/delete の宣言です.

void* operator new(std::size_t) _GLIBCXX_THROW (std::bad_alloc)
  __attribute__((__externally_visible__));
void* operator new[](std::size_t) _GLIBCXX_THROW (std::bad_alloc)
  __attribute__((__externally_visible__));
void operator delete(void*) _GLIBCXX_USE_NOEXCEPT
  __attribute__((__externally_visible__));
void operator delete[](void*) _GLIBCXX_USE_NOEXCEPT
  __attribute__((__externally_visible__));
void* operator new(std::size_t, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT
  __attribute__((__externally_visible__));
void* operator new[](std::size_t, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT
  __attribute__((__externally_visible__));
void operator delete(void*, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT
  __attribute__((__externally_visible__));
void operator delete[](void*, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT
  __attribute__((__externally_visible__));

// Default placement versions of operator new.
inline void* operator new(std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }
inline void* operator new[](std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }

// Default placement versions of operator delete.
inline void operator delete  (void*, void*) _GLIBCXX_USE_NOEXCEPT { }
inline void operator delete[](void*, void*) _GLIBCXX_USE_NOEXCEPT { }

で,C言語malloc/free を使っていた時は malloc(sizeof(Hoge)) って感じで構造体のサイズを指定してたので,嫌でもサイズを意識していたと思うんですが,new/delete 場合も結局サイズを指定するんですね.なので,new Hoge で実際に処理されている内容としては,,,

1.new演算子の探索を行う.
2.sizeof (Hoge) の値を引数にしてnew演算子関数を呼びメモリ確保する.
3.newが返したポインタの指す位置をthisポインタとしてインスタンスを生成する。

です.

new/delete のカスタマイズ

ということで,プレースメント new/delete の話ですが,演算子を定義しなおせばカスタマイズできます.
下記サンプルコードですが,Object というクラスに対して new 演算子を定義しました.std::ostream を引数としてとっているので,new が呼ばれるとコンソール出力されることがわかります.で,プレースメント new を定義したせいで,グローバルスコープのデフォルト new 演算子は探索対象から外れ,そのままでは呼び出すことができなくなってます.

#include <iostream>

class Object {

public:
  static void* operator new(std::size_t size, std::ostream &stream) noexcept(false);

};

void* Object::operator new(std::size_t size, std::ostream &stream) noexcept(false) {
  stream << "operator new" << std::endl;
  return ::operator new(size);
}

int main(int, char**) {

  std::cout << "Hello World!" << std::endl;
  
  // デフォルトの new operator は隠蔽される.
  // Object* obj = new Object;
  
  // ostream を引数として取る new operator
  Object* obj = new(std::cout) Object;

  return 0;
}

で,デフォルトスコープの new 演算子を隠蔽するためにはどうすればいいか.Effective CPPでは,

1.クラスの中で演算子を定義して,デフォルトを呼んであげる.
2.基底クラスにまとめて定義して,派生クラスで using を使って可視にする.

という2つのアプローチでデフォルト new/delete 演算子を定義してました.なお,デフォルトで定義されている new 演算子は次の3つがあるので,すべて使えるようにしてあげないといけません.

  static void* operator new(std::size_t size) noexcept(false);
  static void* operator new(std::size_t size, void* pnt) noexcept(false);
  static void* operator new(std::size_t size, const std::nothrow_t &nothrow) noexcept(true);
#include <iostream>

class Object {
public:
  static void* operator new(std::size_t size) noexcept(false);
  static void* operator new(std::size_t size, void* pnt) noexcept(false);
  static void* operator new(std::size_t size, const std::nothrow_t &nothrow) noexcept(true);
  static void* operator new(std::size_t size, std::ostream &stream) noexcept(false);
};

void* Object::operator new(std::size_t size) noexcept(false) {
  return ::operator new(size);
}

void* Object::operator new(std::size_t size, void* pnt) noexcept(false) {
  return ::operator new(size, pnt);
}

void* Object::operator new(std::size_t size, const std::nothrow_t &nothrow) noexcept(true)  {
  return ::operator new(size, nothrow);
}

void* Object::operator new(std::size_t size, std::ostream &stream) noexcept(false) {
  stream << "operator new" << std::endl;
  return ::operator new(size);
}

class ObjectChild : public Object{
public:
  // 基底クラスの new 関数も探索対象に入れる.
  using Object::operator new;
  static void* operator new(std::size_t size, std::ostream &stream) noexcept(false);
};

void* ObjectChild::operator new(std::size_t size, std::ostream &stream) noexcept(false) {
  return ::operator new(size);
}

int main(int, char**) {

  std::cout << "Hello World!" << std::endl;
  
  // デフォルトの new operator も定義した.
  Object* obj1 = new Object;
  delete obj1;
  
  // ostream を引数として取る new operator
  Object* obj2 = new(std::cout) Object;
  delete obj2;

  // 派生クラスの new operator も定義した.
  ObjectChild* objc1 = new ObjectChild;
  delete objc1;

  // 派生クラスで独自定義した new 演算子.
  ObjectChild* objc2 = new(std::cout) ObjectChild;
  delete objc2;

  return 0;
}

プレースメント new に対応するプレースメント delete の定義

で,プレースメント new を定義した時に,もうひとつ気をつけることがありました.プレースメント new(std::ostream &stream) を定義した場合に.同じシグニチャを持つ delete を定義してあげないと行けないのでした.今回の例では,下記のペアの new/delete を作ってあげる必要があります.

static void* operator new(std::size_t size, std::ostream &stream) noexcept(false);
static void operator delete(void* pnt, std::ostream &stream) noexcept(false);

Hoge* hoge = new Hoge

で,Hoge のコンストラクタが例外を発生させた場合に,new で既に獲得済のリソースを解放する必要がありますが,この時に引数が対応するバージョンの delete がないとメモリがリークしてしまうのでした.該当するサンプルコードを作ってみましたが,確かにメモリリークしてますね.

#include <iostream>

class Object {
public:
  static void* operator new(std::size_t size) noexcept(false);
  static void* operator new(std::size_t size, void* pnt) noexcept(false);
  static void* operator new(std::size_t size, const std::nothrow_t &nothrow) noexcept(true);
  static void* operator new(std::size_t size, std::ostream &stream) noexcept(false);
};

void* Object::operator new(std::size_t size) noexcept(false) {
  return ::operator new(size);
}

void* Object::operator new(std::size_t size, void* pnt) noexcept(false) {
  return ::operator new(size, pnt);
}

void* Object::operator new(std::size_t size, const std::nothrow_t &nothrow) noexcept(true)  {
  return ::operator new(size, nothrow);
}

void* Object::operator new(std::size_t size, std::ostream &stream) noexcept(false) {
  stream << "operator new" << std::endl;
  return ::operator new(size);
}

class ObjectChild : public Object{
public:
  // 基底クラスの new 関数も探索対象に入れる.
  ObjectChild() {
    throw std::exception();
  }
  using Object::operator new;
  static void* operator new(std::size_t size, std::ostream &stream) noexcept(false);
};

void* ObjectChild::operator new(std::size_t size, std::ostream &stream) noexcept(false) {
  return ::operator new(size);
}

int main(int, char**) {

  std::cout << "Hello World!" << std::endl;
  
  ObjectChild *obj2;
  try {
    // ostream を引数として取る new operator
    obj2 = new(std::cout) ObjectChild;
    delete obj2;
  } catch (std::exception e) {
    std::cout << e.what() << std::endl;
  }

  return 0;
}

上記のコードを valgrind で実行すると,確かにメモリリークしていることがわかります.ここで,対応する引数の delete を作ってみましょう.サンプルコードは下記ですが,Exceptionが送出された時点で対応する delete がよばれてメモリリークが避けられていることがわかります.

#include <iostream>

class Object {
public:
  static void* operator new(std::size_t size) noexcept(false);
  static void* operator new(std::size_t size, void* pnt) noexcept(false);
  static void* operator new(std::size_t size, const std::nothrow_t &nothrow) noexcept(true);

  static void operator delete(void* pnt) noexcept(false);
  static void operator delete(void* pnt1, void* pnt2);
  static void operator delete(void* pnt, const std::nothrow_t &nothrow) noexcept(true);

  static void* operator new(std::size_t size, std::ostream &stream) noexcept(false);
  static void operator delete(void* pnt, std::ostream &stream) noexcept(false);
};

void* Object::operator new(std::size_t size) noexcept(false) {
  return ::operator new(size);
}

void* Object::operator new(std::size_t size, void* pnt) noexcept(false) {
  return ::operator new(size, pnt);
}

void* Object::operator new(std::size_t size, const std::nothrow_t &nothrow) noexcept(true)  {
  return ::operator new(size, nothrow);
}

void* Object::operator new(std::size_t size, std::ostream &stream) noexcept(false) {
  stream << "placement new" << std::endl;
  return ::operator new(size);
}

void Object::operator delete(void* pnt) noexcept(false) {
  ::operator delete(pnt);
}

void Object::operator delete(void* pnt1, void* pnt2) noexcept(false) {
  ::operator delete(pnt1, pnt2);
}

void Object::operator delete(void* pnt1, const std::nothrow_t &nothrow) noexcept(true) {
  ::operator delete(pnt1, nothrow);
}

void Object::operator delete(void* pnt, std::ostream &stream) noexcept(false) {
  stream << "placement delete" << std::endl;
  ::operator delete(pnt);
}

class ObjectChild : public Object{
public:
  // 基底クラスの new 関数も探索対象に入れる.
  ObjectChild() {
    throw std::exception();
  }
  using Object::operator new;
  using Object::operator delete;
  static void* operator new(std::size_t size, std::ostream &stream) noexcept(false);
};

void* ObjectChild::operator new(std::size_t size, std::ostream &stream) noexcept(false) {
  return ::operator new(size);
}

int main(int, char**) {

  std::cout << "Hello World!" << std::endl;
  
  ObjectChild *obj2;
  try {
    // ostream を引数として取る new operator
    obj2 = new(std::cout) ObjectChild;
    delete obj2;
  } catch (std::exception e) {
    std::cout << e.what() << std::endl;
  }

  return 0;
}