C++0x の std::shared_ptr
を使う場合は, std::make_shared
を使うことによって,普通にポインタを new
する場合より効率が良く,また安全なコードが書けます:
int main() { auto p = std::make_shared<std::string>( "hoge" ); // std::shared_ptr<std::string> p( new std::string("hoge") ); より効率がよく, // また(ここでは詳しく説明しないが)例外安全で,タイプ数も少ない! }
しかし,これには,配列版が存在しません.
もっと言うなら,「実行時に大きさの変化する領域」は, make_shared
では普通には確保できません.
もちろん,普段は std::vector
を使えばいいので,特に問題にはなりませんが,効率を極限まで突き詰める場合には,やはり不満です.((そもそも効率を極限まで突き詰める場合には shared_ptr
を使うべきではないのかもしれませんが,参照カウントの管理をスレッドセーフに行うのは難しいし,資源の管理方法を自由に扱えるといった利点も shared_ptr
にはあるので, shared_ptr
を使わないことで効率を改善するのは,最後の手段にとっておいたほうがいいと思います.))
というわけで,配列版の make_shared
を制作してみました.
// std::allocate_shared を hack して, // 実行時に確保するメモリ領域の大きさを指定できるようにする #include <memory> #include <cassert> #include <iostream> struct buffer_info_ { void* addr; std::size_t size; }; // C++0x を期待して最小限の実装のみを行う template< class T, class AlignAs = T > struct hacked_allocator { template< class U > struct rebind { typedef hacked_allocator<U, AlignAs> other; }; template< class U, class A_ > friend class hacked_allocator; // info.size には確保する余計なバッファの大きさを指定 // 確保したバッファの大きさは info.addr に格納される explicit hacked_allocator( buffer_info_& info ) : p_( &info ) {} template< class U, class A_ > hacked_allocator( hacked_allocator<U, A_> const& src ) : p_( src.p_ ) {} T* allocate( std::size_t n ) { assert( p_ != 0 ); std::ptrdiff_t const buf_offset = adjust_align_( n * sizeof(T) ); void* const buf = ::operator new ( buf_offset + p_->size ); // p_->addr を経由し,余分に確保したメモリへのアドレスを返す p_->addr = static_cast<char*>(buf) + buf_offset; p_ = 0; // これは本来なら要らない // 間違って二回以上 allocate した場合に検出できるようにしているだけ // デバッグ用情報出力 // std::cout << "allocated: " << buf << std::endl; return static_cast<T*>(buf); } void deallocate( T* p, std::size_t ) { void* const addr = p; ::operator delete( addr ); // デバッグ用情報出力 // std::cout << "deallocated: " << addr << std::endl; } private: buffer_info_* p_; static std::size_t adjust_align_( std::size_t size ) { return ( size + alignof(AlignAs) - 1 ) / alignof(AlignAs) * alignof(AlignAs); } }; // T 型のオブジェクトとバッファを同時に構築する( Alignment 指定あり) template< class T, class AlignAs, class... Args > inline auto make_shared_with_aligned_extra_buffer( std::size_t buffer_size, Args&&... args ) -> std::pair< std::shared_ptr<T>, void* > { buffer_info_ rs = { 0, buffer_size }; auto p = std::allocate_shared<T>( hacked_allocator<T>(rs), std::forward<Args>(args)... ); void* const buf = rs.addr; assert( buf != 0 ); // デバッグ用情報出力 // std::cout << "p.get(): " << static_cast<void*>(p.get()) << std::endl; // std::cout << "buf: " << buf << std::endl; return std::make_pair( std::move(p), buf ); } // T 型のオブジェクトとバッファを同時に構築する( Alignment 無指定) // 今回は使わないが,これもあってよいだろう template< class T, class... Args > inline auto make_shared_with_extra_buffer( std::size_t buffer_size, Args&&... args ) -> std::pair< std::shared_ptr<T>, void* > { return make_shared_with_aligned_extra_buffer<T, T>( buffer_size, std::forward<Args>(args)... ); } // この応用例として,動的配列を make_shared する // …の前に,まず実装用の部品を製作 // 配列に対する RAII を扱うクラス // http://d.hatena.ne.jp/y-hamigaki/20071217/1197892499 の // scoped_placement_array を参考に #include <boost/noncopyable.hpp> template< class T > struct placement_array : private boost::noncopyable { // construct placement_array() noexcept : n_(0) {} // n_ が 0 なら, p_ は何でもいい // destruct ~placement_array() { this->reset(); } // move placement_array( placement_array && src ) noexcept : p_( src.p_ ), n_( src.n_ ) { src.n_ = 0; } // swap void swap( placement_array& other ) noexcept { std::swap( p_, other.p_ ); std::swap( n_, other.n_ ); } // move assignment placement_array& operator=( placement_array && src ) noexcept { this->swap( src ); return *this; } // reset // n_ が 0 なら,何もしない. // そうでなければ,範囲 [ p_, p_ + n_ ) にあるオブジェクトを破棄する void reset() noexcept { // T の dtor は例外を投げないとする std::size_t n = n_; T* p = p_; while( n --> 0 ) { p[n].~T(); } // p_ は変更する必要なし n_ = 0; } // construct void construct( T* p, std::size_t n ) { this->reset(); this->p_ = p; for( ; n_ < n; ++n_ ) { ::new( p + n_ ) T(); } /* // あるいは this->p_ = ::new( static_cast<void*>(p) ) T[n](); this->n_ = n; // こちらのほうが効率が良い上に,少し改良すれば例外安全の強い保証も満たせる. // しかし,この場合, p != this->p_ となる可能性がある( N3290 5.3.4/12 より) // それは都合が悪いので,今のところは,一つ一つ new していく. */ } // その他のメンバは面倒なので後で実装する(しないかも) private: T* p_; std::size_t n_; }; // 配列を構築する template< class T > inline std::shared_ptr<T> make_shared_array( std::size_t n ) { typedef placement_array<T> body_type; auto p_ = make_shared_with_aligned_extra_buffer<body_type, T>( n * sizeof(T) ); auto& p = p_.first; T* const buf = static_cast<T*>( p_.second ); p->construct( buf, n ); return std::shared_ptr<T>( std::move(p), buf ); } // テスト #include <iostream> #include <stdexcept> #include <random> #include <ctime> // [ 0, n ) の乱数を生成 int gen_uniform_int( int n ) { static std::mt19937 gen( static_cast<uint32_t>( std::time(0) ) ); std::uniform_int_distribution<int> dist( 0, n-1 ); return dist(gen); } struct Hoge { int x; Hoge() : x() { // 例外安全性チェック // 10% の確率で例外が発生する if( gen_uniform_int(10) == 0 ) { std::cout << "\n ! ! ! EXCEPTION IS RAISED ! ! ! \n\n"; throw std::runtime_error("exception!"); } std::cout << "Hoge::Hoge() <" << this << ">\n"; } ~Hoge() { std::cout << "Hoge::~Hoge() <" << this << ">\n"; } }; #include <boost/progress.hpp> int main() { // ゼロ要素の配列を確保しても問題ない? auto p0 = make_shared_array<Hoge>(0); std::cout << p0 << std::endl; std::cout << std::endl; // 例外安全性の簡単なチェック for( int i = 0; i < 5; ++i ) { try { auto p = make_shared_array<Hoge>( gen_uniform_int(10) ); } catch( std::exception& e ) { std::cout << std::endl; std::cout << "exception caught. continue\n"; } std::cout << std::endl; } // 速度比較 std::size_t const n = 1000000; std::size_t const m = 10; // int の m 要素の配列を n 回構築/破棄するのにかかった時間を測定する // まず普通に構築する場合 { boost::progress_timer t; for( std::size_t i = 0; i < n; ++i ) { std::shared_ptr<int> p( new int[m](), std::default_delete<int[]>() ); p.reset(); } } // make_shared_array を使った場合 { boost::progress_timer t; for( std::size_t i = 0; i < n; ++i ) { std::shared_ptr<int> p = make_shared_array<int>(m); p.reset(); } } }
以上です(長い….)
一見して,けっこう非効率的に見える部分も多いですが,それらの大半は,安全性のために意識して行っていることです.
また,アライメントも気を使っているので,正直,結構面倒でした.
目に焼き付けておきなさい. メモリ管理をするって,そういうことよ.
さて,このコードの肝は,アロケータの allocate
に対して,ポインタを経由して変数を参照渡しすること.
参照渡しを行えば,本来は得ることのできない情報を 参照経由で取得できるので,
後はその情報を使って好きに料理してやればいい,ということになります.
他にも色々と書きたいことはありますが,面倒なので,この辺でやめておきます.
詳しくはコードを参照して下さい.
で,気になる速度ですが,最適化オプションを( -O1 でもいいので)付けた場合には,きちんと高速化できています.
最適化オプションなしの場合には負けてしまいますが,例外安全のために非効率的に書かざるをえない場所も多いし,仕方ないかと思います.
何か間違いや改善点など,ありましたらコメントよろしくお願いします.