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演算子の探索を行う.
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 ¬hrow) 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 ¬hrow) 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 ¬hrow) 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 のコンストラクタが例外を発生させた場合に,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 ¬hrow) 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 ¬hrow) 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 ¬hrow) 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 ¬hrow) 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 ¬hrow) 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 ¬hrow) 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; }