この章では、コピーできないがムーブできる型として、スマートポインターを説明する。
ストレージを動的確保した場合、解放しなければならない。
void f()
{
int * ptr = new int(0) ;
delete * ptr ;
}
これを正しく行うのは難しい。というのも、動的確保を複数する場合、動的確保が失敗する可能性があるからだ。
void f()
{
int * p1 = new int(0) ;
int * p2 = new int(1) ;
delete p2 ;
delete p1 ;
}
この何気ない一見問題のなさそうなコードには問題がある。もしnew int(1)
が失敗した場合、例外が投げられ、そのまま関数f
の実行は終わってしまう。後続のdelete
は実行されない。
そのような場合にスマートポインターが使える。スマートポインターはポインターの解放とムーブを代わりに行ってくれる便利なライブラリだ。
std::unique_ptr<T>
は以下のように使う。
auto ptr = std::make_unique<型>( 初期化コンストラクターへの引数 )
具体的には以下のようになる。
void f()
{
// std::unique_ptr<int>
auto p1 = std::make_unique< int >( 0 ) ;
auto p2 = std::make_unique< int >( 1 ) ;
}
delete
がないが問題はない。delete
はunique_ptr
のデストラクターが自動で呼んでくれるからだ。
p2
の動的確保が失敗した場合でも問題はない。
unique_ptr
はポインターとほぼ同じように使うことができる。例えばポインターが参照するオブジェクトを間接的に使いたい場合はoperator *
を使う。
int main()
{
auto p = std::make_unique< int >( 0 ) ;
*p = 123 ;
std::cout << *p ;
}
メンバーにアクセスするときにはoperator ->
も使える。
int main()
{
auto p = std::make_unique< std::vector<int> > () ;
p->push_back(0) ;
}
unique_ptr
はたいへん便利なのであらゆる箇所で生のポインターの代わりに使うべきだが、古い関数に生のポインターを渡さなければならない場合などはunique_ptr
を渡せない。そのような場合のためにunique_ptr
、生のポインターを得る方法がある。メンバー関数get
だ。
// 古臭い時代遅れの生ポインターを引数に取る関数
void old_outdated_ugly_function( int * ptr ) ;
int main()
{
auto ptr = std::make_unique<int>(0) ;
old_outdated_ugly_function( ptr.get() ) ;
}
ただしget
を使うときは生のポインターを使う期間がunique_ptr
の寿命の期間内でなければならない。
以下のような場合は使えない。
// 前回渡したポインターの参照する値と
// 今回渡したポインターの参照する値が
// 等しい場合にtrueを返す
int * last_ptr ;
bool is_equal_to_last_ptr( int * ptr )
{
if ( last_ptr == nullptr )
last_ptr = ptr ;
bool b = *ptr == *last_ptr ;
last_ptr = ptr ;
return b ;
}
void f()
{
auto p = std::make_unique<int>(0) ;
is_equal_to_last_ptr( p.get() ) ;
}
int main()
{
f() ;
// エラー
f() ;
}
これは関数f
がunique_ptr
の寿命の期間を超えてポインターを保持して参照しているからだ。
unique_ptr
はコピーができない。
int main()
{
auto p = std::make_unique<int>(0) ;
// エラー、コピーはできない
auto q = p ;
}
これはポインターの値をコピーして、ポインターの所有権を持つオブジェクトが複数存在することを防ぐためだ。
ムーブはできる。
int main()
{
auto p = std::make_unique<int>(0) ;
auto q = std::move(p) ;
}
ムーブしたあとの変数p
はポインターの所有権を持たない。
unique_ptr
の実装はとても簡単だ。例えば簡易的なものならば1ページに収まるほどのコード量で書ける。
template < typename T >
class unique_ptr
{
T * ptr = nullptr ;
public :
unique_ptr() { }
explicit unique_ptr( T * ptr )
: ptr( ptr ) { }
~unique_ptr()
{ delete ptr ; }
// コピーは禁止
unique_ptr( const unique_ptr & ) = delete ;
unique_ptr & operator =( const unique_ptr & ) = delete ;
// ムーブ
unique_ptr( unique_ptr && r )
: ptr( r.ptr )
{ r.ptr = nullptr ; }
unique_ptr & operator = ( unique_ptr && r )
{
delete ptr ;
ptr = r.ptr ;
r.ptr = nullptr ;
}
T & operator * () noexcept { return *ptr ; }
T * operator ->() noexcept { return ptr ; }
T * get() noexcept { return ptr ; }
} ;
コンストラクターでポインターを受け取り、デストラクターで破棄する。コピーは禁止。ムーブは所有権を移動。特に解説するまでもなくコードを読むだけでいいほどの単純な実装だ。
現実のunique_ptr
はもう少し便利な機能を提供しているので、実装はもう少し複雑になっているが、基本的な実装としては変わらない。
unique_ptr
は便利だがコピーができない。コピーができないのはunique_ptr
がポインターの所有権を排他的に独占するからだ。これはどうにもならないが、コピーしたいものはコピーしたい。
そこで、コピーができるスマートポインターとしてshared_ptr
がある。
unique_ptr<T>
はmake_unique<T>(...)
で作るように、shared_ptr<T>
はstd::make_shared<T>(...)
で作る。
int main()
{
auto p = std::make_shared<int>(0) ;
}
unique_ptr
と同じようにポインターのように使うことができる。
shared_ptr
はコピーができる。
auto p1 = std::make_shared<int>(0) ;
auto p2 = p1 ;
auto p3 = p1 ;
しかも、コピーはすべて同じポインターを持っている。例えば以下のようにすると、
*p3 = 123 ;
*p1, *p2, *p3
はいずれも123
になる。
これはどれも同じポインターの値を保持しているためだ。p1.get(), p2.get(), p3.get()
はすべて同じポインターの値を返す。
shared_ptr
は本当に何も考えずに気軽にコピーしてもよい。例えば以下のような本当に汚いコードですら動く。
std::shared_ptr<int> last_ptr ;
bool is_equal_to_last_ptr( std::shared_ptr<int> ptr )
{
if ( last_ptr == nullptr )
last_ptr = ptr ;
bool b = *last_ptr == *ptr ;
last_ptr = ptr ;
return b ;
}
int main()
{
auto p1 = std::make_shared<int>(1) ;
auto p2 = std::make_shared<int>(2) ;
// true
is_equal_to_last_ptr( p1 ) ;
// false
is_equal_to_last_ptr( p2 ) ;
*p2 = 1 ;
// true
is_equal_to_last_ptr( p1 ) ;
}
shared_ptr
はコピーされたすべてのshared_ptr
のオブジェクトが同じポインターを共有する。ポインターを所有する最後のshared_ptr
のオブジェクトが破棄されたときに、ポインターがdelete
される。
そのため、shared_ptr
を使うときは、ポインターが有効なオブジェクトを指すかどうかを気にしなくてよい。そのポインターを所有するshared_ptr
のオブジェクトが1つでも生き残っている限り、ポインターは有効になっている。
shared_ptr
はどうやって実装されているのだろうか。shared_ptr<T>
はT
へのポインターのほかに、現在何個のshared_ptr
のオブジェクトがポインターを所有しているのかを数えるカウンターへのポインターを持っている。
template < typename T >
class shared_ptr
{
T * ptr ;
std::size_t * count ;
} ;
shared_ptr
が初めて作られるとき、このカウンター用にストレージが動的確保され、値が1
になる。
explicit shared_ptr( T * ptr )
: ptr( ptr ), count( new std::size_t(1) )
{ }
コピーされるとき、カウンターがインクリメントされる。
shared_ptr( const shared_ptr & r )
: ptr( r.ptr ), count( r.count )
{
++*count ;
}
デストラクターでは、カウンターがデクリメントされる。そしてカウンターがゼロの場合、ポインターがdelete
される。
~shared_ptr()
{
// カウンターが妥当なポインターを指しているかどうか確認
if ( count == nullptr )
return ;
// デクリメント
--*count ;
// 所有者が0ならば
if ( *count == 0 )
{ // 解放する
delete ptr ;
ptr = nullptr ;
delete count ;
count = nullptr ;
}
}
全体としては少し長いが、以下のようになる。
template < typename T >
class shared_ptr
{
T * ptr = nullptr ;
std::size_t * count = nullptr ;
void release()
{
if ( count == nullptr )
return ;
--*count ;
if ( *count == 0 )
{
delete ptr ;
ptr = nullptr ;
delete count ;
count = nullptr ;
}
}
public :
shared_ptr() { }
explicit shared_ptr( T * ptr )
: ptr(ptr), count( new std::size_t(1) )
{ }
~shared_ptr()
{
release() ;
}
shared_ptr( const shared_ptr & r )
: ptr( r.ptr ), count( r.count )
{
++*count ;
}
shared_ptr & operator =( const shared_ptr & r )
{
if ( this == &r )
return *this ;
release() ;
ptr = r.ptr ;
count = r.count ;
++*count ;
}
shared_ptr( shared_ptr && r )
: ptr(r.ptr), count(r.count)
{
r.ptr = nullptr ;
r.count = nullptr ;
}
shared_ptr & operator =( shared_ptr && r )
{
release() ;
ptr = r.ptr ;
count = r.count ;
r.ptr = nullptr ;
r.count = nullptr ;
}
T & operator * () noexcept { return *ptr ; }
T * operator ->() noexcept { return ptr ; }
T * get() noexcept { return ptr ; }
} ;
これはとても簡易的なshared_ptr
の実装だ。本物のstd::shared_ptr
はもっと複雑で、もっと高度な機能を提供している。