shared_ptrの使い方を知りたかったからいろいろ試してみた

タイトルのとおりです。

基本的な使用方法

まず基本的な使い方。

#include "stdafx.h"
#include <memory>
#include <iostream>

class Hoge {
public:
	Hoge(){}
	~Hoge(){
		std::cout << "Hogeのデストラクタだよ" << std::endl;
	}
	int number_;
};

int _tmain(int argc, _TCHAR* argv[])
{
	{
		std::shared_ptr<Hoge> hoge1(new Hoge); // 初期化

		hoge1->number_ = 30;
		{
			std::shared_ptr<Hoge> hoge2 = hoge1;

			std::cout << "hoge1->number_ : " << hoge1->number_ << std::endl;
			std::cout << "hoge2->number_ : " << hoge2->number_ << std::endl;

		} // ここでhoge2はスコープから抜けるけどhoge1はまだ生きてる
		std::cout << "hoge1->number_ : " << hoge1->number_ << std::endl;

	} // ここでhoge1もスコープから抜けるためHogeのデストラクタが呼ばれる
	return 0;
}

実行結果は以下

hoge1->number_ : 30
hoge2->number_ : 30
hoge1->number_ : 30
Hogeのデストラクタだよ

あるオブジェクトを指し示すポインタがすべてなくなるとそのオブジェクトのデストラクタを呼んでくれるってことみたいです。

初期化の仕方以外は普通のポインタのように扱えます。

当たり前ですがshared_ptrを使わないとメモリリークが発生しますね。

#include "stdafx.h"
#include <memory>
#include <iostream>

class Hoge {
public:
	Hoge(){}
	~Hoge(){
		std::cout << "Hogeのデストラクタだよ" << std::endl;
	}
	int number_;
};

int _tmain(int argc, _TCHAR* argv[])
{
	{
		Hoge* hoge1= new Hoge;

		hoge1->number_ = 30;
		{
			Hoge* hoge2 = hoge1;

			std::cout << "hoge1->number_ : " << hoge1->number_ << std::endl;
			std::cout << "hoge2->number_ : " << hoge2->number_ << std::endl;

		} 
		std::cout << "hoge1->number_ : " << hoge1->number_ << std::endl;

	} // ここでhoge1がスコープを抜けるけどデストラクタは呼ばれない
	return 0;
}

実行結果

hoge1->number_ : 30
hoge2->number_ : 30
hoge1->number_ : 30

Hogeのデストラクタが呼ばれていないのが分かります。

参照はずし

参照はずしも普通のポインタ同様に行えます

#include "stdafx.h"
#include <memory>
#include <iostream>

class Hoge {
public:
	Hoge(){}
	~Hoge(){
		std::cout << "Hogeのデストラクタだよ" << std::endl;
	}
	int number_;
};

int _tmain(int argc, _TCHAR* argv[])
{
	{
		std::shared_ptr<Hoge> hoge1(new Hoge);
		hoge1->number_ = 15;

		Hoge hoge2 = *hoge1;

		std::cout << "hoge1->number_ : " << hoge1->number_ << std::endl;
		std::cout << "hoge2.number_ : " << hoge2.number_ << std::endl;

		std::cout << std::endl << "hoge2.number_ = 3" << std::endl << std::endl;
		hoge2.number_ = 3;

		std::cout << "hoge1->number_ : " << hoge1->number_ << std::endl;
		std::cout << "hoge2.number_ : " << hoge2.number_ << std::endl;

	}
	return 0;
}

実行結果

hoge1->number_ : 15
hoge2.number_ : 15

hoge2.number_ = 3

hoge1->number_ : 15
hoge2.number_ : 3
Hogeのデストラクタだよ
Hogeのデストラクタだよ

use_count()で参照しているshared_ptrの数を調べる

hoge1->とするとHogeへのポインタとして使えますが、
hoge1.とするとshared_ptrとしての機能を呼び出すことが出来ます。

hoge1.use_count()でオブジェクトを指し示しているshared_ptrの数を知ることができます。

#include "stdafx.h"
#include <memory>
#include <iostream>

class Hoge {
public:
	Hoge(){}
	~Hoge(){
		std::cout << "Hogeのデストラクタだよ" << std::endl;
	}
	int number_;
};

int _tmain(int argc, _TCHAR* argv[])
{
	{
		std::shared_ptr<Hoge> hoge1(new Hoge);
		hoge1->number_ = 15;

		std::cout << "hoge1.use_count() : " << hoge1.use_count() << std::endl;

		std::shared_ptr<Hoge> hoge2 = hoge1;
		std::cout << "hoge1.use_count() : " << hoge1.use_count() << std::endl;
		std::cout << "hoge2.use_count() : " << hoge2.use_count() << std::endl;


	}
	return 0;
}

実行結果

hoge1.use_count() : 1
hoge1.use_count() : 2
hoge2.use_count() : 2
Hogeのデストラクタだよ

注意しなければならないのは、"あるアドレスを指すshared_ptrの数"に基づいているわけじゃなくて、"あるshared_ptrが何個コピーされたか"に基づいてる(?)ってことです。
あんまりうまく説明できている気がしないですが、何がいいたいかというと、次のようなコードをかいちゃだめだよってことです。

#include "stdafx.h"
#include <memory>
#include <iostream>

class Hoge {
public:
	Hoge(){}
	~Hoge(){
		std::cout << "Hogeのデストラクタだよ" << std::endl;
	}
	int number_;
};

int _tmain(int argc, _TCHAR* argv[])
{
	Hoge* hoge = new Hoge;
	hoge->number_ = 11;
	std::shared_ptr<Hoge> hoge1(hoge);
	{
		std::shared_ptr<Hoge> hoge2(hoge); // 同じアドレスを指すshared_ptrが二つあるけど、

		std::cout << "hoge1.use_count() : " << hoge1.use_count() << std::endl;
		std::cout << "hoge2.use_count() : " << hoge2.use_count() << std::endl;

	} // ここでhoge2がスコープをぬけてデストラクタを呼んでしまう!

	std::cout << hoge1->number_ << std::endl; // 削除済みの領域にアクセスしようとしてエラーになる
	return 0;
}
hoge1.use_count() : 1
hoge2.use_count() : 1
Hogeのデストラクタだよ
-572662307
Hogeのデストラクタだよ

意外だったのですが、削除済みの領域のデストラクタを呼ぼうとするとちゃんと呼ばれるんですね。オブジェクトがどうこうってんじゃなくて型の情報から呼ばれるんでしょうか。

なんしか、同じアドレスを指していても、代入とかの方法で明示的に同じであるってことをshared_ptrに伝えてやらないといけないってことです。

あと、今みたいなコードは書くべきじゃないですね。

.get()で生のアドレスを取得

hoge1.get()で生のアドレスを取得できるそうです。

#include "stdafx.h"
#include <memory>
#include <iostream>

class Hoge {
public:
	Hoge(){}
	~Hoge(){
		std::cout << "Hogeのデストラクタだよ" << std::endl;
	}
	int number_;
};

int _tmain(int argc, _TCHAR* argv[])
{
	Hoge* hoge;
	{
		std::shared_ptr<Hoge> hoge1(new Hoge); 
		hoge1->number_ = 5;

		hoge = hoge1.get();
		hoge->number_ = 10; // 取得した生アドレスを介して操作

		std::cout << "hoge1->number_ : " << hoge1->number_ << std::endl; // 変更が反映されてる
		std::cout << "hoge->number_ : " << hoge->number_ << std::endl;
	} 

	std::cout << "hoge->number_ : " << hoge->number_ << std::endl; // 削除済みの領域にアクセス

	return 0;
}

実行結果

hoge1->number_ : 10
hoge->number_ : 10
Hogeのデストラクタだよ
hoge->number_ : -572662307

当たり前ですが、.get()で生のアドレスを取得したからってshared_ptrはそんなことにお構いなくスコープを抜けた時点でデストラクタを呼んでくれます。

うまくデストラクタを呼んでくれないとき(循環参照)

shared_ptrがうまくデストラクタを呼んでくれないことがあります。
循環参照が存在するとそれらのデストラクタは呼ばれないそうです。

#include "stdafx.h"
#include <memory>
#include <iostream>

class Hoge {
public:
	Hoge(){}
	~Hoge(){
		std::cout << "Hogeのデストラクタだよ" << std::endl;
	}
	int number_;
	std::shared_ptr<Hoge> hoge_;
};

int _tmain(int argc, _TCHAR* argv[])
{
	{
		std::shared_ptr<Hoge> hoge1(new Hoge);
		std::shared_ptr<Hoge> hoge2(new Hoge);

		// 循環参照
		hoge1->hoge_ = hoge2;
		hoge2->hoge_ = hoge1;
	}// スコープを抜けてもdeleteが呼ばれない


	return 0;
}

実行結果

(デストラクタが呼ばれていない)

リンクドリストの先頭と末尾をつないだりするとこれが起こりそうですね。

vectorにつっこんでみる

std::vectorに入れたりすると、(他から参照されていないかぎり)eraseするだけでデストラクタも呼んでくれます。

#include "stdafx.h"
#include <memory>
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
class Hoge {
public:
	Hoge(int i) : number_(i){}
	~Hoge(){
		std::cout << "Hogeのデストラクタだよ" << std::endl;
	}
	int number_;
	std::shared_ptr<Hoge> hoge_;
};

int _tmain(int argc, _TCHAR* argv[])
{
	std::vector<std::shared_ptr<Hoge> > hoges;
	hoges.push_back(std::shared_ptr<Hoge>(new Hoge(0)));
	hoges.push_back(std::shared_ptr<Hoge>(new Hoge(1)));
	hoges.push_back(std::shared_ptr<Hoge>(new Hoge(2)));
	hoges.push_back(std::shared_ptr<Hoge>(new Hoge(3)));
	hoges.push_back(std::shared_ptr<Hoge>(new Hoge(4)));

	std::tr1::function<void(std::shared_ptr<Hoge>)> PrintNumbers
		= [](std::shared_ptr<Hoge> h)->void{std::cout << h->number_ << std::endl;};
	std::for_each(hoges.begin(), hoges.end(), PrintNumbers);

	auto p = hoges.begin();
	hoges.erase(p + 2); // 他に参照されていないのでeraseした時点でデストラクタが呼ばれる

	std::shared_ptr<Hoge> hoge1(hoges.at(1));
	hoges.erase(p + 1); // hoge1も参照しているのでeraseしてもデストラクタは呼ばれない

	std::for_each(hoges.begin(), hoges.end(), PrintNumbers);

	return 0;
}

実行結果

0
1
2
3
4
Hogeのデストラクタだよ
0
3
4
Hogeのデストラクタだよ
Hogeのデストラクタだよ
Hogeのデストラクタだよ
Hogeのデストラクタだよ

.reset()でリセット

.reset()を呼ぶと、呼んだ変数がオブジェクトを指し示しているっていう情報がリセットされ、参照カウントが一つ減ります。
.reset()を呼んだときに参照カウントが0になったらデストラクタが呼ばれますよ。

#include "stdafx.h"
#include <memory>
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
class Hoge {
public:
	Hoge(){}
	~Hoge(){
		std::cout << "Hogeのデストラクタだよ" << std::endl;
	}
	int number_;
	std::shared_ptr<Hoge> hoge_;
};

int _tmain(int argc, _TCHAR* argv[])
{
	{
		std::shared_ptr<Hoge> hoge1(new Hoge);
		std::shared_ptr<Hoge> hoge2 = hoge1;
		hoge1->number_ = 3;

		hoge1.reset();
		std::cout << "hoge2->number_ : " << hoge2->number_ << std::endl;

		hoge2.reset();
		std::cout << "まだスコープぬけてないよ" << std::endl;
	}

	return 0;
}

実行結果

hoge2->number_ : 3
Hogeのデストラクタだよ
まだスコープぬけてないよ

ちょっと分かりにくいですがスコープを抜ける前に.reset()で参照カウントが0になりデストラクタが呼ばれているのが確認できますね。

まとめ

こんな感じでしょうか。

勉強になりました。