C++プログラマーよ!std::make_sharedを安易に使うべからず!

boost::shared_ptrstd::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 newoperator 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コンストラクタを使う方はintnewと制御ブロック(参照カウンター等を管理する領域)の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を使わなくなってしまうという問題も。こちらはコンパイルが通ってしまうので、さらに注意が必要です。

もう一つオマケに

 make_sharedと同様の関数に、allocatorを指定できるallocate_sharedがあります。このallocatorオブジェクトは、その名に反してコンストラクタ呼び出し、デストラクタ呼び出し、メモリ解放、メモリ再配置までの動作を指定できます。make_sharedでは不十分ないろいろキモい事をやりたい場合は、こちらをお使い下さい。

C++プログラミング入門

C++プログラミング入門

Effective C++ 原著第3版 (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

Effective C++ 原著第3版 (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)