底辺大学の院生がプログラミングや機械学習を勉強するブログ

勉強していることを雑にまとめるブログ。当然、正しさの保証は一切しない。

スマートポインタ

qiita.com

スマートポインタについての知識がないC++使いは嫌らしいので猛省して勉強することとする。

スマートポインタ概要

ポインタを用いてメモリを動的確保をする場合、解放のし忘れが無いようにコーディングしなければならないことは周知の事実である。
ただし人間がコードを書く以上、どうしても解放のし忘れが起こりうる。
そこで、自動的にメモリの解放をさせようという仕組みがスマートポインタである。

C++11で利用できるスマートポインタは何種類かあるが、中には使うことが奨励されていないスマートポインタ(auto_ptr)もあるので、調べたところ主に使われている3種類について、使い方とその違いをまとめる。
なお、スマートポインタを利用するためにはmemoryヘッダーをインクルードする必要がある。

unique_ptr

先ほど述べた、よく使われる3種の中では最も基本的なスマートポインタがunique_ptrだ。
使い方はこんな感じ。

std::unique_ptr<int> int_ptr(new int(3)); // 宣言
std::cout << *int_ptr << std::endl;       // 間接参照演算子でアクセス

宣言のところだけが生のポインタと異なる部分で、その他使い方は同じ。
関節参照演算子*でデータが参照できるし、クラスであればアロー演算子->でメンバ関数へのアクセスや関数の呼び出しが行える。

これを見てわかる通り、スマートポインタはただのテンプレートクラスだ。
イメージとしては、unique_ptrクラスがポインタとそれに対する処理をラップしていて、コンストラクタで確保したメモリのアドレスを渡して管理を委託し、オブジェクトがスコープから外れると自動的に呼びだされたunique_ptrのデストラクタでポインタがdeleteされる感じか。仕組みがわかれば意外と単純な作り。

ただ、unique_ptrにはこれに加えて、オブジェクトをコピーできないという特徴を持つ。
つまり、こういうことができない。

std::unique_ptr<int> int_ptr1(new int(3));
std::unique_ptr<int> int_ptr2(int_ptr1);   // error
std::unique_ptr<int> int_ptr3 = int_ptr1;  // error

スマートポインタには所有権という概念があって、この例ではnew int(3)で確保したメモリの所有権はint_ptr1が持っている。
unique_ptrはその名の通り、複数のunique_ptrが1つのメモリの所有権を持つことを禁止しているユニークなスマートポインタだ。
だから、上の例ではint_ptr1が所有権を持つメモリの所有権をint_ptr2は持つことが出来ず、コンパイルエラーとなる。

ただし所有権の移動(ムーブ)はできる。

std::unique_ptr<int> int_ptr1(new int(3));
std::unique_ptr<int> int_ptr2(std::move(int_ptr1));  // ムーブ
std::unique_ptr<int> int_ptr3 = std::move(int_ptr2); // ムーブ

ムーブは所有権をムーブ先に委譲し、ムーブ元は管理していたポインタについての関連の一切を断つ。
故に、所有権は1つのunique_ptrしか持たず、所有権のユニークさは保たれる。


メモリの確保のし直しや解放はreset関数で行える。

std::unique_ptr<int> int_ptr1(new int(3));
std::cout << *int_ptr1 << std::endl; // '3'と表示
int_ptr1.reset(new int(5));          // 確保のし直し
std::cout << *int_ptr1 << std::endl; // '5'と表示
int_ptr1.reset();                    // 解放
std::cout << *int_ptr1 << std::endl; // error

メモリを確保し直した場合でも古いメモリは解放されるので安心。

shared_ptr

基本的にはunique_ptrでいいと思う。ただ、設計上複数のスマートポインタで同一のメモリを管理したい場面もあると思う。
そこで登場するのがshared_ptrだ。

基本的な使い方はunique_ptrと同じだけど、shared_ptrはunique_ptrで禁止されていたコピーができるようになってる。

std::shered_ptr<int> int_ptr1(new int(3));
std::shered_ptr<int> int_ptr2(int_ptr1);   // ok
std::shered_ptr<int> int_ptr3 = int_ptr1;  // ok

// make_shared
std::shared_ptr<int> int_ptr4 = std::make_shared<int>(3);

内部的には同一のメモリの所有権を持っているスマートポインタの数がカウントされていて、カウントが0になると自動的に解放が行われるらしい。

また、実用の際はint_ptr4のようにmake_sharedを使ったほうが高速らしい。
ただあんまり使うなって言っている人もいるしこの辺は議論の余地がありそう。

cflat-inc.hatenablog.com

weak_ptr

shared_ptrには循環参照という弱点がある。
循環参照は、AがBのshared_ptrを保持し、BもまたAのshared_ptrを保持するような状況を指す。
この場合、Aが所有権を破棄してもBがAの所有権を保持しているためAのデストラクタは呼ばれず、
またBが所有権を破棄してもAがBの所有権を保持しているため、やはりBのデストラクタは呼ばれない。
結果として、最後までAとBは解放されず、メモリリークとなる。

この問題を解決できるスマートポインタがweak_ptrだ。

weak_ptrはそれ自身はメモリの所有権を持たないが、shared_ptrの指すメモリを参照することができる。
使用の際は、参照している間に参照元のshared_ptrが解放されないようにlock関数を使ってアクセスする。

auto shared_p = std::make_shared<int>(3);
std::weak_ptr<int> weak_p = shared_p;

if (auto ptr = weak_p.lock())
{
	std::cout << *ptr << std::endl;
}

使い分けは?

スマートポインタそのものについても含めてこのページが詳しい。
qiita.com


簡単に書くと、基本的にはunique_ptrを使い、
どうしても複数のスマートポインタで管理したい場合は、使用している間は絶対に解放されてほしくないポインタについてはshared_ptrを使い、
参照できなくても問題のない場合weak_ptrを使うといった感じか。



うーむスマートポインタ……存在は知ってたけどこんなに奥が深いとは。
ただ使い方は簡単だしコードの書き換えもの負担も大きくないから、これからはちゃんと使っていこう。

おまけ

昨日の乱数を生成する関数オブジェクトをスマートポインタで書く。

// TEMPLATE CLASS UniformRand
template <class Ty_ = int>
class UniformRand
{	// wrap random number generator
	// from uniform integer distribution as function object
private:
	std::unique_ptr<std::mt19937> mt_;
	std::unique_ptr<std::uniform_int_distribution<Ty_>> ud_;

public:
	explicit UniformRand(const Ty_ min = 0,
		const Ty_ max = std::numeric_limits<Ty_>::max(),
		const long seed = static_cast<long>(time(0))) :
		ud_(new std::uniform_int_distribution<Ty_>(min, max)),
		mt_(new std::mt19937(seed))
	{	// construct from parameters
	}

	void init(const Ty_ min, const Ty_ max,
		const long seed = static_cast<Ty_>(time(0)))
	{	// initialize
		ud_.reset(new std::uniform_int_distribution<Ty_>(min, max));
		mt_.reset(new std::mt19937(seed));
	}

	Ty_	operator()(void) const
	{	// return random value
		return ud_->operator()(*mt_);
	}
};