ホームに戻る
出典 :
関連 :
目次 :
(広義の)スマートポインタとは
動的メモリ(ヒープ)を安全に使用するための機構。
C/C++(およびD言語)では確保したヒープを解放し忘れると、その分だけ使用できるメモリが減る(メモリリーク)。
スマートポインタを使用すると、スコープを抜ける際に自動的にヒープが解放されるため、解放忘れによるメモリリークの危険性が減る。
RAIIはスマートポインタを実現する方法の一つである。
またSTLのコンテナにおけるヒープはいずれもスマートポインタとして実装されているため、ヒープを明示的に解放する必要は無い。
C++における(狭義の)スマートポインタ
C++では std 名前空間に、auto_ptr<T> 、unique_ptr<T> 、shared_ptr<T> 、weak_ptr<T> のスマートポインタが定義されている。
使用する際は #include <memory> が必要。
いずれもテンプレートであるため、任意の型を指し示すポインタとして利用可能である。
いずれも T* 型のポインタを保持し、デストラクタで自身の所有するメモリを解放する。
注意が必要な点
スマートポインタには「所有権」の概念がある。これは、「そのメモリにアクセスする権利と、解放する責務」を表す。
確保したメモリをスマートポインタに渡すと、スマートポインタはそのメモリの所有権を得、
所有権を保持している(スマートポインタが生存している)間はスマートポインタを介してメモリにアクセスできる。
スマートポインタが寿命を終える(スコープを抜ける)際にメモリが解放される。
逆に言えば、所有権を保持している間はメモリが解放されることは無い(明示的に解放する場合を除く)。
auto_ptr<T> (C++11以降は非推奨)
C++03以前でも使用できる基本的なスマートポインタ。
#include<iostream>
#include<memory>
int main()
{
// int型のメモリを動的に確保し、その所有権をauto_ptrに委ねる
std::auto_ptr<int> ptr(new int(10));
// * 演算子で生ポインタのようにアクセスできる
for( int i = 0; i<10; ++i )
{
*ptr+=i;
}
std::cout << "ptr=" << *ptr << std::endl;
return 0;
} //< スコープを抜ける際にメモリを解放
auto_ptr<T> には以下のような深刻な問題点が存在するため、C++11以降では非推奨となっている。
- コピーによって、所有権がコピー先に移動する
- 内部処理でコピーを行うため、コンテナに入れることができない
- 配列を保持できない
- deleterを指定できない
unique_ptr<T> (C++11以降)
確保したメモリに対する所有権を持つスマートポインタが唯一であることを保証する。以下のような特徴を持ち、auto_ptr<T> の欠点をカバーしている。
- あるメモリの所有権を持つ unique_ptr<T> は、ただ一つに限られる
- コピーができない(ムーブは可能)
- 生ポインタと遜色がないほど処理が速い
- 配列を保持できる
- deleterを指定できる
C++11以降であれば、特殊な用途を除いて広範に用いることができる。
ポインタの保持するメモリへのアクセスは、生ポインタと同様 * 演算子や -> 演算子を用いる。
C++14以降であれば、 make_unique<T>() を用いるとインスタンスの生成とメモリ確保を同時に行えるほか、
初期値からインスタンスの型を推論できるので簡便である。
初期化・ムーブ
// コンストラクタの引数として、動的確保したメモリのアドレスを指定
std::unique_ptr<int> ptr(new int(10));
// reset() 関数を使って、後から代入することもできる
std::unique_ptr<int> ptr2;
ptr2.reset(new int(10));
// make_unique() 関数を使用した初期化(C++14以降)
auto ptr3 = std::make_unique<int>(10);
// ポインタに配列を割り当てる
std::unique_ptr<int[]> ptr_arr1(new int[10]);
auto ptr_arr2 = std::make_unique<int[]>(10); //< C++14以降、括弧内は配列のサイズ
// コピーは不可
std::unique_ptr<int> ptr2(ptr); //< コピーコンストラクタによるコピー ⇒ ERROR
std::unique_ptr<int> ptr3;
ptr3 = ptr; //< コピー代入演算子によるコピー ⇒ ERROR
// ムーブは可能(所有権が移動する)
std::unique_ptr<int> ptr4(std::move(ptr)); //< ムーブコンストラクタによるムーブ ⇒ ptr が保持していた所有権が ptr4 に移動
std::unique_ptr<int> ptr5;
ptr5 = std::move(ptr4); //< ムーブ代入演算子によるムーブ ⇒ ptr4 が保持していた所有権が ptr5 に移動
明示的なメモリの解放
通常はデストラクタでメモリが解放されるが、明示的に開放したい場合は reset() を用いる。
std::unique_ptr<int> ptr2(new int(10));
// 引数なし、または nullptr 引数での reset() ⇒ メモリ解放
ptr2.reset();
ptr2.reset(nullptr);
所有権の有無を確認
operator bool() を用いる。所有権を持つ場合には true 、持たない場合には false が返る。
std::unique_ptr<int> ptr;
if(ptr)
{
// (ptr == true)
// 所有しているときの処理
}
// bool変数への代入でも、所有権の有無を取得可能
bool CanAccess=ptr;
生ポインタへの変換
- get() : 生ポインタを得る。所有権は std::unique_ptr<T> が保持したまま
- release() : 生ポインタを得る。所有権を std::unique_ptr<T> から奪う ⇒ 自身で解放する必要あり
// get() : ポインタの所有権は unique_ptr が保持し続ける
int* pint;
pint = ptr.get();
// release() : ポインタの所有権を unique_ptr から奪う
pint = ptr.release();
delete pint;
配列の保持
unique_ptr<T[]> で配列を保持できる。添字演算子(operator[])を用いて、通常の配列と同様にアクセスできる。
{
std::unique_ptr<int[]> ptrArray(new int[10]);
for(int i = 0; i < 10; ++i)
{
ptrArray[i]=i;
}
} //< 配列型の場合、スコープを抜ける際に自動的に delete[] が呼ばれる
deleterの指定
メモリを解放する際に、delete (または delete[] )以外を用いる必要がある場合に、解放処理を関数オブジェクトにより指定できる。
// deleter用の関数オブジェクト
struct fclose_delete
{
// deleterの実体 ( () 演算子関数として実装)
void operator()(FILE *fp) const
{
fclose(fp);
std::cout << "deleter" << std::endl;
}
};
int main()
{
// ファイルポインタを使用してファイルを開く
const char *file = "C:\\test.txt";
FILE *fpTmp;
if( fopen_s(&fpTmp, file, "r") != 0 )
{
std::cout << file << "が開けません。" << std::endl;
std::cin.get();
return 0;
}
// ファイルから1文字読み出す
std::cout << static_cast<char>(fgetc(fpTmp)) << std::endl; //< OK
{
// スマートポインタ fp の宣言と初期化
// (テンプレート第2引数にdeleter関数オブジェクトを指定)
std::unique_ptr<FILE, fclose_delete> fp(fpTmp);
std::cout << static_cast<char>(fgetc(fpTmp)) << std::endl; //< OK
std::cout << static_cast<char>(fgetc(fp.get())) << std::endl; //< OK
} //< fp の寿命ここまで
// deleterによってファイルが閉じられているためエラーとなる
std::cout << static_cast<char>(fgetc(fpTmp)) << std::endl; //< NG
std::cin.get();
}
C:\test.txt の内容
ABCDEFG
実行結果
A
B
C
deleter
・
shared_ptr<T> (C++11以降)
あるメモリの所有権を独占する unique_ptr<T> とは異なり、メモリの所有権を複数で共有できる。
具体的には、以下のような仕組みで動作する。これはガベージコレクションに類似する。
- shared_ptr<T> には所有権を持つポインタの数を記憶するカウンタが存在する
- コピーされることでカウンタがインクリメントされる
- デストラクタや明示的解放により、カウンタがデクリメントされる
- カウンタがゼロとなった際に、メモリが実際に解放される
shared_ptr<T> は以下のような特徴を持つ。
- コピー、ムーブともに可能
- 内部でカウンタを保持するため、生ポインタと比較すると低速である
- 配列を保持できる。ただし、明示的に deleter を指定する必要がある
初期化・コピー・ムーブ
初期化の手段は unique_ptr<T> と同様。
尚 shared_ptr<T> は値のためのメモリ領域とともに、参照カウンタのためのメモリ領域を確保する。
make_shared<T>() を用いるとこれらを同時に行えることから効率が良い。こちらはC++11でも使用できる。
// コンストラクタの引数として、動的確保したメモリのアドレスを指定
std::shared_ptr<int> ptr(new int(10));
// reset() 関数を使って、後から代入することもできる
std::shared_ptr<int> ptr2;
ptr2.reset(new.int.10);
// make_shared() 関数を使用した初期化
auto ptr3 = std::make_shared<int>(10);
// ポインタに配列を割り当てる
std::shared_ptr<string[]> ptr_arr1(new string[10]); //< C++17以降
auto ptr_arr2 = std::make_shared<string[]>(10); //< C++20以降、括弧内は配列のサイズ
// コピー
std::shared_ptr<int> ptr2(ptr); //< コピーコンストラクタによるコピー ⇒ ptr2 が新たに所有権を保持
std::shared_ptr<int> ptr3;
ptr3 = ptr; //< コピー代入演算子によるコピー ⇒ ptr3 が新たに所有権を保持
// ムーブ(所有権が移動する)
std::shared_ptr<int> ptr4(std::move(ptr)); //< ムーブコンストラクタによるムーブ ⇒ ptr が保持していた所有権が ptr4 に移動
std::shared_ptr<int> ptr5;
ptr5 = std::move(ptr4); //< ムーブ代入演算子によるムーブ ⇒ ptr4 が保持していた所有権が ptr5 に移動
// unique_ptr<T> からのムーブも可能( unique_ptr<T> は所有権を失う)
std::unique_ptr<int> uptr(new int(10));
std::shared_ptr<int> sptr(std::move(uptr)); //< ムーブコンストラクタによるムーブ ⇒ uptr が保持していた所有権が sptr に移動
std::unique_ptr<int> uptr2(new int(10));
std::shared_ptr<int> sptr2;
ptr2 = std::move(uptr2); //< ムーブ代入演算子によるムーブ ⇒ uptr2 が保持していた所有権が sptr2 に移動
所有権の確認
unique_ptr<T> 同様 operator bool() で所有権の有無を取得できる。
また、use_count() で自身が保持するメモリの所有権を持つ(自身を含めた)ポインタの数を、
unique() で自身が保持するメモリの所有権を持つポインタが自身のみかを調べることができる。
即ち、
ptrunique() == (ptruse_count() == 1)
循環参照
shared_ptr<T> ではコピーが可能であることから、循環参照と呼ばれる事象が発生し得る。
循環参照が発生した場合、本来なら安全に解放されるはずのメモリが解放されず、メモリリークとなる。
#include<memory>
class hoge
{
public:
std::shared_ptr<hoge> ptr;
};
int main()
{
auto pHoge1 = std::make_shared<hoge>(); //< Hoge1
auto pHoge2 = std::make_shared<hoge>(); //< Hoge2
// Hoge1.ptr が Hoge2 の所有権を取得
pHoge1->ptr = pHoge2;
// Hoge2.ptr が Hoge1 の所有権を取得
pHoge2->ptr = pHoge1;
return 0;
} //< pHoge1 、pHoge2 のデストラクタが呼ばれるが…
この例において、pHoge1 、pHoge2 が指す実体を Hoge1 、Hoge2 とする。
pHoge1 、pHoge2 のデストラクタが呼ばれる直前、Hoge1 の所有権は pHoge1 と Hoge2.ptr が、Hoge2 の所有権は pHoge2 と Hoge1.ptr が有している。
ここで pHoge1 のデストラクタが呼ばれると、Hoge1 の所有権を放棄する。しかし Hoge2.ptr が Hoge1 の所有権を保持しているため、Hoge1 のデストラクタは呼ばれない。
同様に pHoge2 のデストラクタが呼ばれても Hoge1.ptr が Hoge2 の所有権を保持しているため、Hoge2 のデストラクタは呼ばれない。
即ち、Hoge1 、Hoge2 ともに解放されず、メモリリークとなる。
weak_ptr<T> (C++11以降)
shared_ptr<T> を用いることで生じ得る循環参照への対処として導入されたスマートポインタ。
前述のものと異なり、メモリの所有権を持たないが、shared_ptr<T> の指すメモリを参照することができる。
shared_ptr<T> により循環参照となるコードを weak_ptr<T> で書き直すと以下のようになる。
#include<memory>
class hoge
{
public:
// shared_ptr<T> では循環参照となるため、weak_ptr<T> を用いる
std::weak_ptr<hoge> ptr;
};
int main()
{
std::shared_ptr<hoge> pHoge1 = std::make_shared<hoge>(); //< Hoge1
std::shared_ptr<hoge> pHoge2 = std::make_shared<hoge>(); //< Hoge2
// Hoge1のメンバ変数で、pHoge2を参照する
pHoge1->ptr = pHoge2;
// Hoge2のメンバ変数で、pHoge1を参照する
pHpge2->ptr = pHoge1;
return 0;
}
shared_ptr<int> の場合と異なり、メモリの所有権は pHoge1 と pHoge2 だけが有しているため、正しく解放される。
weak_ptr<T> は以下のような特徴を持つ。
- shared_ptr<T> が所有権を持つメモリしか管理できない
- コピー、ムーブともに可能(ムーブ時は参照を失う)
初期化・コピー・ムーブ
std::shared_ptr<int> sptr = std::make_shared<int>(10);
// コンストラクタ、代入演算子で shared_ptr を受け取る
std::weak_ptr<int> wptr(sptr);
std::weak_ptr<int> wptr2;
wptr2 = wptr;
// コピー
std::weak_ptr<int> wptr3(wptr); //< コピーコンストラクタによるコピー
std::weak_ptr<int> wptr4;
wptr4 = wptr; //< コピー代入演算子によるコピー
// ムーブ
std::weak_ptr<int> wptr5(std::move(wptr4)); //< ムーブコンストラクタによるムーブ ⇒ wptr4 は参照を失う
std::weak_ptr<int> wptr6;
wptr6 = std::move(wptr5); //< ムーブ代入演算子によるムーブ ⇒ wptr5 は参照を失う
参照するメモリへのアクセス
unique_ptr<T> や shared_ptr<T> と異なり、* 演算子や -> 演算子は使用できない。
参照するためにはblock()関数により、参照先である shared_ptr<int> を取得し、そこからアクセスする。
(参照中にメモリが解放されるのを防ぐため。)
std::shared_ptr<int> sptr=std::make_shared<int>(10);
std::weak_ptr<int> wptr(sptr);
{
// lock関数によって、参照先を保持する shared_ptr を取得する
std::shared_ptr<int> ptr = wptrlock();
}
まとめ
ポインタの指す対象を「所有」(解放する責任を有する)しているのか、単に「参照」しているのかによってスマートポインタの使い分けが必要となる。
また、対象のメモリが、アクセス中に解放されることが無い「安全なアクセス」か、アクセス中でも解放され得る「危険なアクセス」かも考慮する必要がある。
生ポインタは「安全なアクセス」による「参照」、または外部APIの利用上やむを得ない場合に限定して用いるのが正しい。ほとんどの場合はスマートポインタで代替できる。
特徴および機能一覧
|
auto_ptr<T> |
unique_ptr<T> |
shared_ptr<T> |
weak_ptr<T> |
適する場面 |
なし (C++11以降では使用が推奨されない) |
メモリを「所有」する必要がある場合 |
以下のいずれかに該当する場合
- 複数のオブジェクトによって「所有」されるのが最も自然な場合
- 「危険なアクセス」による「参照」が必要な場合
|
「危険なアクセス」により参照される場合 |
所有権 |
唯一 |
唯一 |
複数 |
なし |
参照 |
* , -> |
* , -> |
* , -> |
lock() による 間接参照 |
初期化 |
コンストラクタ |
コンストラクタ |
コンストラクタ |
コンストラクタ (shared_ptr を受け取る) |
|
reset() |
reset() |
reset() |
|
make_unique() (C++14以降) |
make_shared() |
|
コピー |
可(所有権が移動) |
不可 |
可(所有権が分散) |
可(参照が分散) |
ムーブ |
可(所有権が移動) |
可(所有権が移動) |
可(所有権が移動) (unique_ptr からのムーブ可) |
可(参照が移動) |
明示的解放 |
不可 |
reset() |
不可 |
reset() (参照の解放のみ) |
deleter定義 |
不可 |
可 |
可 (make_shared()との併用不可) |
不可 |
配列の保持 |
不可 |
可 |
可 (deleter要定義) |
可 |
operator*()() |
可 |
可 |
可 |
不可 |
operator[](size_t) |
不可 |
可 |
不可 |
不可 |
operator bool() |
不可 |
可 |
可 |
不可 |
reset() |
不可 |
可 |
可 |
可 |
get() |
不可 |
可 |
可 |
不可 |
release() |
不可 |
可 |
不可 |
不可 |
use_count() |
不可 |
不可 |
可 |
可 |
unique() |
不可 |
不可 |
可 |
不可 |
expired() |
不可 |
不可 |
不可 |
可 |
lock() |
不可 |
不可 |
不可 |
可 |
備考 |
C++11以降非推奨 |
|
循環参照が発生しうる |
|