C++11スマートポインタ入門

  • 416
    いいね
  • 4
    コメント

概要

 C++11では、unique_ptr<T> shared_ptr<T> weak_ptr<T>の3種のスマートポインタが新たに追加された。これらのスマートポインタは、いずれもメモリの動的確保の利用の際に生じる多くの危険性を低減する目的で使用されるが、それぞれ独自の考え方と機能を持っている。3種のスマートポインタを適切に使い分けることで、安全性と開発速度の向上が見込めるだけでなく、プログラマの意図に合わせて「ポインタ」を記述し分けることができる、非常に強力なツールとなる。

 本解説では、スマートポインタについて初めて学ぶ人を対象に、C++11で追加された3種のスマートポインタの機能と使い方、および3種をどのように考えて使うかについて、初歩的な解説を行う。

導入

スマートポインタとは

 C++では、メモリの動的確保に new deleteを用いる。しかし、newによって確保したメモリをうっかりdeleteし忘れると、確保したメモリを開放し忘れる「メモリリーク」が起きてしまう。

sample_raw_ptr1.cpp
#include<iostream>
int main(){
   int ptr=new int(10);

   for(int i=0;i<10;++i){
      *ptr+=i;
   }
   std::cout<<"ptr="<<*ptr<<std::endl;

   delete ptr; //これを忘れると、メモリリーク

   return 0;
}

また、 newしていないポインタに対する deleteや、すでに解放したメモリをさらに deleteしようとする操作は未定義である。

sample_raw_ptr2.cpp
#include<memory>
class hoge{
private:
   int* ptr;
public:
   hoge(int val_){
      if(val_>0)ptr=new int(val_);
   }
   ~hoge(){delete ptr;}
};

int main(){
   //hogeのコンストラクタでnewが呼ばれない
   hoge Hoge(-1);

   return 0;
}//=== ERROR=== ディストラクタでdeleteが呼ばれるので、動作は未定義!

 このような、メモリの解放忘れや、不適切な対象への deleteの実行等をなくすために、確保したメモリを自動的に解放してくれるクラスが考え出された。このようなクラスを、 スマートポインタと呼ぶ。 int*のような生のポインタではなく、スマートポインタを介してメモリを扱うことで、動的確保の利用の際に生じるメモリの解放忘れ等の危険性を低減することができる。

スマートポインタの仕組み

 スマートポインタには、一般にメモリの 所有権 という考え方がある。これは「そのメモリにアクセスする権利と、開放する義務」のことである。簡単に言ってしまうと、所有権を保持するとは「そのメモリは俺が管理するから、俺から使え & 勝手に消すな」という意味である。

 確保したメモリをスマートポインタに渡すと、スマートポインタはそのメモリに対する所有権を得る。所有権を保持している間は、そのスマートポインタを介してメモリにアクセス可能であり、解放されることはない。

 所有権を持つスマートポインタが全て破棄されれば、そのメモリは誰からも使用されていないことが保障できる。そこで、スマートポインタは ディストラクタを用いて所有権を持つメモリが不要となる瞬間を判断し、解放を自動的に行う。生成されたオブジェクトは、破棄される際に(わずかな例外を除いて)必ずディストラクタが呼ばれるので、メモリの解放忘れを避けることができる。

auto_ptr

 C++03までは、スマートポインタとして auto_ptr<T> が用意されていた。これはT*型のポインタを保持し、ディストラクタ時に自身が所有権を持つメモリが存在すれば、 delete を実行する、というシンプルなものである。

sample_auto_ptr.cpp
#include<iostream>
#include<memory>
int main(){
   //int型のメモリを動的に確保し、その所有権をauto_ptrに委ねる
   std::auto_ptr<int> ptr(new int(10));

   //operator*()で生ポインタのようにアクセスできる
   for(int i=0;i<10;++i){
      *ptr+=i;
   }
   std::cout<<"ptr="<<*ptr<<std::endl;

   return 0;
}//ディストラクタ内で、自動的にptrの中身をdelete

このように、 auto_ptr<T>を使えば deleteを記述する必要がなくなり、解放忘れという単純なミスをなくすことができる。

 しかし、 auto_ptr<T>には、深刻な問題点が複数指摘されている。

  • コピーによって コピー先にメモリの所有権が移動する。このため、気が付かない間に保持している auto_ptr<T>がアクセス不能になっている場合がある。
sample_auto_ptr_error.cpp
#include<iostream>
#include<memory>
class hoge{
private:
   std::auto_ptr<int> ptr;
public:
   hoge(int val_):ptr(new int(val_)){}
   int getValue()const{return *ptr;}
};

int main(){
   //hogeのコンストラクタでint型を動的に確保しauto_ptrに委ねる
   hoge Hoge(10);

   //コピーコンストラクタで二つ目のhogeを作成
   //この時Hoge.ptrからHoge.ptr2に所有権が移動!
   hoge Hoge2(Hoge);

   //Hogeの値を呼び出す
   Hoge.getValue();  //===ERROR===
   //実行時エラー!
   //Hoge.ptrはすでに所有権を失っているので、アクセスできない

   return 0;
}
  • 内部処理でコピーを行うため、コンテナ( std::vector<T>等)に入れることができない。
  • 配列を保持することができない。
  • deleter(後述)を指定することができない。

 これらの問題点のため、C++11以降では auto_ptr<T>の使用は非推奨となっており、代わりとして後述する新たな3種のスマートポインタが追加された。このため、C++11以降が使える環境下では、 auto_ptr<T>を使用してはいけない。仮に使わざるを得ない場合にも、使用には細心の注意を要する。

unique_ptr

unique_ptrとは

 unique_ptr<T>は、あるメモリに対する所有権を持つポインタが、ただ一つであることを保証するようなスマートポインタである。 auto_ptr<T>同様に、テンプレート引数で保持するポインタ型を指定し、スマートポインタが破棄される際にディストラクタにおいて自動的にメモリを開放する。 unique_ptr<T>は、以下の様な特徴を持っている。

  • あるメモリの所有権を持つ unique_ptr<T>は、 ただ一つのみである。
  • コピーが出来ない。代わりに、C++11で新たに追加されたムーブによって、所有権を移動することができる(ムーブについては、諸先輩方の記事参照。例えば右辺値参照ムーブセマンティクスについての記事など)。
  • 通常のポインタに匹敵する処理速度。
  • 配列を扱うことができる。
  • deleter(後述)を指定することができる。

 先ほどの auto_ptr<T>で問題が生じたプログラムを unique_ptr<T>を使って書き直してみよう。

sample_unique_ptr.cpp
#include<iostream>
#include<memory>
class hoge{
private:
   std::unique_ptr<int> ptr;
public:
   hoge(int val_):ptr(new int(val_)){}
   int getValue()const{return *ptr;}
};

int main(){
   //hogeのコンストラクタでint型を動的に確保しunique_ptrに委ねる
   hoge Hoge(10);

   //unique_ptrはコピーできないので、
   //コピーコンストラクタで作成しようとすると、コンパイルエラー
   //hoge Hoge2(Hoge);  //===ERROR===

   //明示的にmoveするならOK
   hoge Hoge2(std::move(Hoge));

   return 0;
}//auto_ptr同様、ディストラクタで自動的にメモリ解放

このように、コピーを禁止することで、気が付かない間に所有権を失ってしまうのを防止するなど、 auto_ptr<T>の欠点を解決した設計となっている。

unique_ptrの使い方

 詳しい使い方を見てみよう。なお、使用の際には #include <memory>を指定する必要がある。

まず、unique_ptr<T>にメモリの所有権を委ねるには、コンストラクタで指定するか、 reset(pointer)を使う。C++14以降では、make_unique<T>関数を使って作成することができる。

//コンストラクタの引数として、動的確保したメモリのアドレスを指定
std::unique_ptr<int> ptr(new int(10));

//reset関数を使って、後から代入することもできる
std::unique_ptr<int> ptr2;
ptr2.reset(new int(10));

//C++14以降であれば、make_unique関数を使うこともできる
std::unique_ptr<int> ptr3=std::make_unique<int>(10);

unique_ptr<T>は、コピーは禁止されているが、ムーブは使用することができる。

std::unique_ptr<int> ptr(new int(10));

//コピーコンストラクタや、コピー代入演算子はエラー
//std::unique_ptr<int> ptr2(ptr); //===ERROR===
std::unique_ptr<int> ptr3;
//ptr3=ptr;   //===ERROR===

//ムーブコンストラクタや、ムーブ代入演算子はOK
//この時、所有権が移動する
std::unique_ptr<int> ptr4(std::move(ptr)); //ok ptrの所有権がptr4に移動する
std::unique_ptr<int> ptr5;
ptr5=std::move(ptr4);   //ok ptr4の所有権がptr5に移動する

メモリの解放は、ディストラクタや reset(pointer)を使う。

{
   std::unique_ptr<int> ptr(new int(10));
}//ここでptrのディストラクタが呼ばれ、自動的に解放される

//引数なしやnullptrを引数としてreset関数を呼んでも、明示的に解放できる
std::unique_ptr<int> ptr2(new int(10));
ptr2.reset();

所有権を実際に保持しているかの判定には、 operator bool()を使う。所有権を持つ場合にはtrue、持たない場合にはfalseを返す。

std::unique_ptr<int> ptr;
//メモリの所有権を保持しているかどうかは、boolの文脈で使用することで判定できる
//所有していれば、trueを返す
if(ptr){
   //---所有しているときの処理---
}

//bool変数への代入でも、所有権の有無を取得可能
bool CanAccess=ptr;

ポインタの保持するメモリにアクセスするには、通常のポインタ同様に operator*()operator->()が使用できる

std::unique_ptr<std::string> pStr(new std::string("test"));

//operator*()でstring型を参照
// "test" と表示される
std::cout<<*pStr<<std::endl;        

//operator->()で、string型のsize関数を呼び出せる
unsigned int StrSize = pStr->size();  

生のポインタが欲しいときには、get()release()を使う。 get()は生ポインタを得るだけで、ポインタの所有権は unique_ptr<T>が保持し続ける。 release()はポインタの所有権自体も放棄するため、メモリの開放は自分で行う必要がある。

//通常のポインタがほしい時には、get関数を使う。
//ポインタの所有権はunique_ptrが保持し続ける
int* pint;
pint = ptr.get();

//所有権自体を放棄する場合は、release関数を使う
//この場合、メモリの開放自体は自分で行う必要がある
pint = ptr.release();
delete pint;

unique_ptr<T[]>のように指定すれば、配列を扱うこともできる。配列型の場合、 operator[](size_t)を使用することができる。

{
   //型名[]をテンプレート引数に指定することで、配列も扱える
   std::unique_ptr<int[]> ptrArray(new int[10]);

   //配列型の場合operator[](size_t)を使うことができる
   for(int i=0;i<10;++i){
      ptrArray[i]=i;
   }

}//配列型の場合、自動的にdelete[]が呼ばれて開放される。

deleterの指定

 メモリを開放するための処理は、 delete以外を用いる必要がある場合も考えられる。例えば、メモリはある関数を介して必ず確保、開放しなければいけない状況を考えよう。

memory_from_storage.hpp
//int型のリソースをstorageから確保する
int* malloc_int_from_storage();
//int型のリソースをstorageから開放する
void free_int_from_storage(int* ptr);

このような場合、メモリの開放には deleteではなく、 free_int_from_storage関数を使いたい。

 deleterとは、このようなメモリ解放時の処理を指定する、関数オブジェクトのことである。 unique_ptr<T>では、第二テンプレート引数に指定することで、deleterを指定することができる。

sample_deleter.cpp
#include<memory>
#include"memory_from_storage.hpp"
//free_int_from_storageを使ってメモリを開放する関数オブジェクトを定義する。
struct deleter_for_storage{
   void operator()(int* ptr_){
      free_int_from_sotrage(ptr_);
   }
};
int main(){
   //テンプレート第二引数で、deleterを指定する
   std::unique_ptr<int, deleter_for_storage> ptr(malloc_int_from_storage());

   //deleteではなく、free_int_from_storageがメモリ解放の際に呼ばれる。
   return 0;
}

deleterを明示的に指定してやれば、 new以外の方法で確保したメモリや、あるいはメモリ以外のリソースについても、 unique_ptr<T>を用いて管理することができる。

shared_ptrとweak_ptr

shared_ptrとは

 あるメモリの所有権を持つ unique_ptr<T>がただ一つに限られていたのに対し、同一のメモリの所有権を複数で共有できるようにしたスマートポインタが、shared_ptr<T>である。
 
 具体的には、次のような仕組みである。 shared_ptr<T>は、所有権を持つポインタの数を記録するカウンタを持っている。所有権を持つ shared_ptr<T>がコピーされると、内部でカウンタがインクリメントされ、ディストラクタや明示的解放時にデクリメントされる。全ての所有者がいなくなると、カウンタがゼロとなり、メモリが開放される。カウンタで所有者数を管理することで、複数の shared_ptr<T>が所有権を保持していても、適切なタイミングで一度だけメモリ解放が実行されるのである。

  shared_ptr<T>は、以下のような特徴を持つ。

  • あるメモリの所有権を、複数の shared_ptr<T>共有することができる。メモリの開放は、全ての所有権を持つ shared_ptr<T>が破棄された際に実行される。
  • コピーもムーブも可能
  • 内部でカウンタを利用している関係上、やや通常のポインタよりメモリ確保やコピー等の処理が遅い。
  • 配列を扱うことができる。ただし、明示的にdeleterを指定する必要がある。

 実際に、使ってみよう。

sample_shared_ptr.cpp
#include<memory>
#include<iostream>
int main(){
   //空のshared_ptrを作成
   std::shared_ptr<int> ptr;

   {
      //intの所有権を持つ、ptr2を作成
      std::shared_ptr<int> ptr2(new int(0));

      //ptr2の所有権をptrにコピー、共有する
      ptr=ptr2;

      *ptr+=10;
      *ptr2+=10;

   }//ここで、ptr2のディストラクタが呼ばれる
    //ptrも同一のメモリに対する所有権を持っているため、まだ解放はされない

   //当然、ptrはまだ使用可能
   std::cout<<"ptr="<<*ptr<<std::endl;  //"ptr=20"と出力

}//ここで、ptrのディストラクタが呼ばれる
 //メモリの所有権を持つポインタがいなくなったので、解放される

このように、 shared_ptr<T>を利用することで、複数のポインタから同一のメモリを安全に利用し、破棄できるようになる。

shared_ptrの使い方

 詳しい使い方を見てみよう。 unique_ptr<T>同様、使用の際には #include <memory>を指定する必要がある。

 shared_ptr<T>unique_ptr<T>同様にコンストラクタで指定するか、 reset(pointer)を使うことでメモリの所有権を委ねる事ができる。ただ、 shared_ptr<T>は所有するメモリだけでなく自身のカウンタも動的にメモリを確保する必要があるため、これらのメモリ確保を同時に行える make_shared<T>(args...)を使って作成した方が処理効率が良い。可能な限りこちらを使うべきである。

//コンストラクタや、reset関数を使ってのメモリ割り当てが可能
std::shared_ptr<int> ptr(new int(10));
std::shared_ptr<int> ptr2;
ptr2.reset(new int(10));

//make_shared関数を使うと、効率よくメモリを確保できる(C++11から使える)
std::shared_ptr<int> ptr3=std::make_shared<int>(10);

//複数の引数を持つコンストラクタも、make_sharedから呼び出せる
typedef std::pair<int,double> int_double_t;
std::shared_ptr<int_double_t> ptr4=std::make_shared<int_double_t>(10,0.4);

shared_ptr<T>は、コピー、ムーブともに使用することができる。

//コピーコンストラクタや、コピー代入演算子もOK
//所有権は、ptr、ptr2、ptr3の三者が保持する
std::shared_ptr<int> ptr=std::make_shared<int>(10);
std::shared_ptr<int> ptr2(ptr); //ok ptrとptr2で所有権を共有
std::shared_ptr<int> ptr3;
ptr3=ptr; //ok ptrとptr3で所有権を共有

//ムーブコンストラクタや、ムーブ代入演算子はOK
//この時、所有権は移動する
std::shared_ptr<int> ptr4(std::move(ptr)); //ok ptrの所有権がptr4に移動する
std::shared_ptr<int> ptr5;
ptr5=std::move(ptr2);   //ok ptr2の所有権がptr5に移動する

unique_ptr<T>からムーブすることも可能。この時、もちろん unique_ptr<T>は所有権を失う。

//コンストラクタでunique_ptrからムーブ
//所有権がuptrからptrに移動する
std::unique_ptr<int> uptr(new int(10));
std::shared_ptr<int> ptr(std::move(uptr));

//代入演算子で、ムーブ
//こちらも同様に、所有権がuptr2からptr2に移動する
std::unique_ptr<int> uptr2(new int(10));
std::shared_ptr<int> ptr2;
ptr2=std::move(uptr2);

所有権の放棄は、ディストラクタや reset(pointer)で行われる。ただし、実際にメモリが解放されるのは、そのメモリの所有権を持つポインタが全て破棄された場合である。

std::shared_ptr<int> ptr=std::make_shared<int>(10);
{
   //ptrから所有権をコピー
   std::shared_ptr<int> ptr2(ptr);

}//ここでptrのディストラクタが呼ばれ、ptr2は所有権を放棄
 //ptrがまだ所有権を保有しているので、メモリは解放されていない

//引数なしやnullptrを引数としてreset関数を呼んでも、明示的に所有権を放棄できる
//ptrが所有権を放棄すると、所有権を持つポインタがなくなるので、ここでメモリ解放
ptr.reset();

所有権を実際に保持しているかの判定には、 operator bool()を使う。所有権を持つ場合にはtrue、持たない場合にはfalseを返す。また、 use_count()を使って自身が保持するメモリに所有権を持つポインタの数を、 unique()を使って自身が保持するメモリに所有権を持つポインタが唯一(自分だけ)かどうかを、調べることができる。

std::shared_ptr<int> ptr;

//メモリの所有権を保持しているかどうかは、boolの文脈で使用することで判定できる
//所有していれば、trueを返す
if(ptr){
   //---所有しているときの処理---
}

//bool変数への代入も可能
bool CanAccess=ptr;

//所有者の数を確認するには、use_count関数を使う
std::cout<<"use_count="<<ptr.use_count()<<std::endl;

//所有者が唯一であることを確認するには、unique関数を使う
//use_count()==1ならtrue, それ以外ならfalseとなる
if(ptr.unique()){
   std::cout<<"unique"<<std::endl; 
}

ポインタの保持するメモリにアクセスするには、通常のポインタ同様に operator*()operator->()が使用できる。

std::shared_ptr<std::string> pStr=std::make_shared<std::string>("test");

//operator*()でstring型呼び出し
// "test" と表示される
std::cout<<*pStr<<std::endl;        

//operator->()で、string型のsize関数を呼び出せる
unsigned int StrSize=pStr->size();  

生のポインタが欲しいときには、get()を用いる。 unique_ptr<T>と違って、複数のポインタが所有権を保持しているので、 所有権のみを放棄するrelease()は用意されていない。

std::shared_ptr<int> ptr=std::make_shared<int>(10);

//通常のポインタがほしい時には、get関数を使う。
//ポインタの所有権はshared_ptrが保持し続ける
int* pint;
pint=ptr.get();

shared_ptr<T>は、配列を扱うこともできる(shared_ptr<T[]>でない点に注意)。ただし、 operator[](size_t)は用意されていない。また、 deleterを明示的に指定する必要がある。なお、deleterを明示的に指定する際には、 make_shared<T>(args...)は使えない。

{
   //[]型名をテンプレート引数に指定することで、配列も扱える
   //第2引数で、配列用にdeleterを指定
   //deleterを明示的に指定する際には、make_sharedは使えない
   std::shared_ptr<int> ptrArray(new int[10], std::default_delete<int[]>());

   //operator[]は使えない
   //代わりに、get関数からアクセスはできる
   for(int i=0;i<10;++i){
      //ptrArray[i]=i;      //===ERROR=== 
      ptrArray.get()[i]=i;  //ok
   }

}//default_delete<int[]>を指定しておけば、自動的にdelete[]が呼ばれて開放される。

循環参照

 コピーが禁止されていた unique_ptr<T>と違って、 shared_ptr<T>はポインタのコピーができ、しかも安全にメモリも解放される、非常に有用なツールである。しかし、 shared_ptr<T>には、循環参照と呼ばれる厄介な状況が生じうることが知られている。

sample_circular_reference.cpp
#include<memory>
class hoge{
public:
   std::shared_ptr<hoge> ptr;
};
int main(){
   std::shared_ptr<hoge> pHoge1=std::make_shared<hoge>();
   std::shared_ptr<hoge> pHoge2=std::make_shared<hoge>();

   //Hoge1のメンバ変数で、pHoge2を参照する
   pHoge1->ptr=pHoge2;
   //Hoge2のメンバ変数で、pHoge1を参照する
   pHpge2->ptr=pHoge1;

   return 0;
}//shared_ptrのディストラクタが呼ばれるのに、確保した二つのhogeが解放されない。

この時、hoge確保した二つのhogeは解放されない。

この原因は、少し考えてみるとわかる。

pHoge1、pHoge2が指すメモリに確保されたHogeを、それぞれHoge1、Hoge2と呼ぼう。

ディストラクタが呼ばれる直前、Hoge1はpHoge1とHoge2.ptrが、Hoge2はpHoge2とHoge1.ptrがそれぞれ所有権を持っている。

まず、pHoge1のディストラクタが呼ばれると、Hoge1への所有権を放棄する。しかし、この時Hoge2.ptrはHoge1への所有権を保持しているので、Hoge1のディストラクタは呼ばれない。

次に、pHoge2のディストラクタが呼ばれると、Hoge2への所有権を放棄する。
しかし、先ほど同様、Hoge2はHoge1.ptrも所有権を保持しているので、Hoge2のディストラクタは呼ばれない。

結果として、shared_ptrが最初に所有権を委ねられたHoge1, Hoge2は、最後まで解放されないままになってしまう。

 このような、相互に参照した状態のことを 循環参照と呼ぶ。循環参照が発生すると、 shared_ptr<T>によって安全に管理されているはずのメモリに、メモリリークが発生することがあるのである。

weak_ptrとは

  weak_ptr<T>は、循環参照によって生じる問題を防ぐために導入されたスマートポインタである。先の二つのスマートポインタと違い、 weak_ptr<T>メモリへの所有権を持つことはない。その代わりに、 weak_ptr<T>shared_ptr<T>の指すメモリを参照することができる

 先ほどの循環参照の例で見てみよう。

sample_weak_ptr.cpp
#include<memory>
class hoge{
public:
   //shared_ptrで所有権を得る代わりに、weak_ptrで参照する
   std::weak_ptr<hoge> ptr;
};
int main(){
   std::shared_ptr<hoge> pHoge1=std::make_shared<hoge>();
   std::shared_ptr<hoge> pHoge2=std::make_shared<hoge>();

   //Hoge1のweak_ptrで、pHoge2を参照する
   pHoge1->ptr=pHoge2;
   //Hoge2のweak_ptrで、pHoge1を参照する
   pHpge2->ptr=pHoge1;

}//所有権は、pHoge1, pHoge2しかそれぞれ持っていないので、正しく解放される

今回は、確保したメモリへの所有権は、pHoge1, pHoge2しか持っていないので、循環参照が生じず、正しく解放される。

 このように、 weak_ptr<T>は所有権を持たずに メモリへの参照のみ保持することによって、確保したメモリへアクセス可能とするスマートポインタである。所有権を持たないため自身が参照しているメモリが解放されてしまうことはあるが、 weak_ptr<T>は、すでに shared_ptr<T>によって参照先のメモリが解放されたかどうかを確認することができる。

weak_ptrの使い方

 具体的な使い方を見てみよう。

 weak_ptr<T>は shared_ptr<T>が所有権を持つメモリしか管理できない。

//コンストラクタや代入演算子で、shared_ptrを受け取る
std::shared_ptr<T> sptr=std::make_shared<int>(10);
std::weak_ptr<T> wptr(sptr); 
std::weak_ptr<T> wptr2;
wptr2=sptr;

//ポインタを直接受け取ることはできない
//std::weak_ptr<T> wptr3(new int(10)); //===ERROR===

weak_ptr<T>は、コピー、ムーブともに使用することができる。

std::shared_ptr<int> sptr=std::make_shared<int>(10);
std::weak_ptr<int> wptr(sptr);

//コピーコンストラクタや、コピー代入演算子もOK
std::weak_ptr<int> wptr2(wptr);
std::weak_ptr<int> wptr3;
wptr3=wptr;

//ムーブコンストラクタや、ムーブ代入演算子もOK
//この時、wptr2,wptr3は参照を失う
std::weak_ptr<int> wptr4(std::move(wptr2));
std::weak_ptr<int> wptr5;
wptr5=std::move(wptr3);

参照の開放は、ディストラクタや reset()を使う。

std::shared_ptr<int> sptr=std::make_shared<int>(10);
{
   std::weak_ptr<int> wptr(sptr);
}//ディストラクタで参照を開放

std::weak_ptr<int> wptr2(sptr);

//reset関数で明示的に解放
wptr2.reset();

所有権はそもそも保持していないので、 operator bool()は使えない。参照先が解放されていないかどうかは、 expired()を使う。戻り値がtrueならすでに解放されている。また、 use_count()を使って参照先のメモリに所有権を持つ shared_ptr<T>の数を調べることができる。

std::shared_ptr<int> sptr=std::make_shared<int>(10);
std::weak_ptr<int> wptr(sptr);

//参照先のメモリが解放されていないかどうかはexpired関数を使う
if(wptr.expired()){
   std::cout<<"expired"<<std::endl;
}

//所有者の数を確認するには、use_count関数を使う
std::cout<<"use_count="<<wptr.use_count()<<std::endl;

参照するメモリにアクセスするのに、 operator*()operator->()は使えない。メモリにアクセスするためには、まず lock()によって参照先を保持する shared_ptr<T>を取得し、そこからアクセスする。これは、使用中に解放されてしまうのを避けるためである。

std::shared_ptr<int> sptr=std::make_shared<int>(10);
std::weak_ptr<int> wptr(sptr);
{
   //lock関数によって、参照先を保持するshared_ptrを取得する
   std::shared_ptr<int> ptr=wptr.lock();
}

生のポインタと3つのスマートポインタの使い分け

 ここまで、性質の違う3種のスマートポインタ、 unique_ptr<T> shared_ptr<T> weak_ptr<T>を見てきた。これに加えて、従来の生のポインタも存在する。これらは、どのように使い分ければいいのだろう? この問題を考えるために、ポインタの利用方法における二つの側面に注目してみよう。

 一つ目は、ポインタの指す対象を 「所有」しているのか、単に 「参照」しているのか、である。所有している、すなわち自身が解放する責任を有する動的確保したメモリを指すためにポインタを使っているのであれば、それは「所有」のポインタである。一方、単にアクセスするためだけにしか使わないのであれば、それは「参照」である。

 一般に、あるオブジェクトを「所有」するのは単一のオブジェクトであるが、まれに 複数のオブジェクトが単一のオブジェクトを「所有」するのが自然である場合もある。例えば、複数のperson(人物)で構成されるcommittee(委員会)クラスを考えた場合、committeeはpersonを「所有」するのは自然な設計である。しかし、あるpersonが複数のcommitteeに参加する場合には、複数のcommitteeによって所有権は共有されることになる。

 二つ目は、ポインタの指す対象へのアクセスが、アクセス中に解放されることのない 「安全なアクセス」か、解放される可能性がある 「危険なアクセス」か、である。自身が所有するポインタへのアクセスや、所有者より内側のスコープで利用している場合は、基本的に「安全なアクセス」だといえる。一方で、アクセスしようとするメモリが、いつ解放されるか分からない他のオブジェクトが所有権を持つ場合、その利用は「危険なアクセス」であるといえる。

 これら二つの側面から、ポインタの使い方をいくつかに分類することができる。あくまで一つの指針としてであるが、以下のような基準で使い分けるとよいのではないか、と筆者は考えている。

  1. メモリを動的確保によって「所有」する必要がある場合、原則として unique_ptr<T>を利用する。
    • ただし、メモリへのアクセスは所有者のメンバ関数内に限るなど、「危険なアクセス」による「参照」が必要ない設計にしておく。
  2. メモリを動的確保によって「所有」するが、以下のいずれかの場合には shared_ptr<T>を利用する。
    • 複数のオブジェクトによって「所有」されるのが、最も自然な設計である場合。
    • 「危険なアクセス」による「参照」が、設計上どうしても必要な場合。
  3. 「危険なアクセス」による「参照」には、weak_ptr<T>を利用する。
  4. 「安全なアクセス」による「参照」か、外部のAPIの利用する際にどうしても必要である場合に限り、生のポインタを使ってよい。それ以外では原則として生ポインタを使ってはいけない

もちろん、これはあくまで参考となる指針であって、明確なルールとなるものではないと思っている(こんな基準じゃだめだ、もっとこうした方がいい、という意見があれば、ぜひコメント頂けるとありがたいです)。

 重要なことは、これまで単に生のポインタで記述していたものを、これら三種のスマートポインタも使って書き分けることができる、ということである。スマートポインタ自体の機能に加えて、 プログラマーがどういった意図で使用しているのかをより明確に記述することで、動的メモリの利用が孕む危険性は大きく減らすことができるだろう。

参考文献

cppreference.com
cpprefjp - C++ Reference Site
EZ-NET C++ プログラミング : C++11でスマートポインタを使用する
kikairoyaの日記 : ポインタは用法用量を守って正しくお使いください
本の虫 : rvalue reference 完全解説
yohhoyの日記(別館): 本当は怖くないムーブセマンティクス
Qiita : shared_ptrとweak_ptrの使い分けわかる?