C++プログラマーよ!std::make_sharedを安易に使うべからず!
boost::shared_ptr
やstd::shared_ptr
の便利さは、boostやC++11に慣れた方であれば当然ご存知かと思います(もちろん、循環参照の危険性や、それを回避するためのweak_ptr
の使い方まで含めて)。
しかし世の中というものはさらに便利にできていて、この便利なshared_ptr
をさらに便利に扱うためにmake_shared
という関数が用意されています!
make_sharedは何が素晴らしいのか
C++に限らず、メモリ確保というのは一般に極めて重い処理になります。よって、new
の回数をなるべく少なくするようにプログラムを書くのが高速化のカギになります。
さてそこで、試しに次のようなコードを書いてみましょう。
#include <stdlib.h> #include <memory> #include <iostream> static const char *log_title = nullptr ; void *operator new (std::size_t size) { void *ptr = malloc(size) ; if (log_title) std::cout << log_title << ": new " << ptr << "(" << size << "bytes)" << std::endl ; return ptr ; } void operator delete(void *ptr) { if (log_title) std::cout << log_title << ": delete " << ptr << std::endl ; free(ptr) ; } int main() { log_title = "std::shared_ptr<int>(new int())" ; { auto ptr = std::shared_ptr<int>(new int()) ; } log_title = "std::make_shared<int>()" ; { auto ptr = std::make_shared<int>() ; } log_title = nullptr ; return 0 ; }
operator new
とoperator delete
をオーバーロードして、log_title
が非nullptr
の時だけnew
/delete
のログを取るようにしています(常時ログONだと、main()初期化前後に標準ライブラリ内で呼ばれるnew
/delete
に反応するので)。より厳密には、operator new
内でもっといろいろやらねばならないのですが、本題とは関係ないので省略。
結果は一目瞭然(gcc4.8.1にて確認)。
std::shared_ptr<int>(new int()): new 0x7fbda34000e0(4bytes) std::shared_ptr<int>(new int()): new 0x7fbda34039e0(24bytes) std::shared_ptr<int>(new int()): delete 0x7fbda34000e0 std::shared_ptr<int>(new int()): delete 0x7fbda34039e0 std::make_shared<int>(): new 0x7fbda34039e0(32bytes) std::make_shared<int>(): delete 0x7fbda34039e0
shared_ptr
のコンストラクタを使う方はint
のnew
と制御ブロック(参照カウンター等を管理する領域)のnew
が走りますが、make_shared
の場合は何と、int
と制御ブロックを結合した領域のnew
が1回だけ!
合計バイト数が違うのは、単にアライメントの問題で、make_shared
のせいではありません。shared_ptr
に入れるのをint
ではなくlong long
(8バイト整数)にすると、どちらもちゃんと合計32バイトになります。
make_sharedが使えない場合
と、一見素晴らしい高速化のカギに見えるmake_shared
ですが、shared_ptr
のコンストラクタは使える場合でも、make_shared
を使うとコンパイルエラーとなる場合もあります。
次のコードを見て下さい。
class Hoge { private: Hoge() {} public: static std::shared_ptr<Hoge> create() { return std::shared_ptr<Hoge>(new Hoge()); } // OK //static std::shared_ptr<Hoge> create() { return std::make_shared<Hoge>(); } // NG };
このクラスHoge
は、必ずshared_ptr
としてインスタンスを生成する必要があります。このためHoge
のコンストラクタはprivate
となっており、static
なメンバー関数create()
を使う事によりshared_ptr
を生成します。さて……ここが落とし穴。
コンパイルしてみると、コンストラクタ版の方は正常に動作して、make_shared
版はコンパイルすら通りません。というのもmake_shared
は確保したいクラスと制御ブロックを結合するために、両者をメンバー変数として持った新しいクラスを作るからです。しかしHoge
のコンストラクタはprivate
なので、実際にはそのようなクラスは作れません。
もちろん、Hoge
の中でその新しいクラスに対してfriend
宣言をすればこれは回避できるのですが、このクラスの定義は標準ライブラリの実装依存なので、事実上できないと言っていいでしょう。
他にも、operator new
/delete
をオーバーロードしたクラスにmake_shared
を使うと、そのクラスのoperator new
/delete
を使わなくなってしまうという問題も。こちらはコンパイルが通ってしまうので、さらに注意が必要です。