0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C++】shared_ptrはガベージコレクションではない:循環参照とラムダが生むリークの正体

0
Posted at

C++でメモリ管理に悩んだ経験がある人なら、一度はこう思ったことがあるはずだ。

shared_ptr を使っていればメモリリークは起きないのでは…?

しかし、実際にはshared_ptrを使用していても、デストラクタが呼ばれないオブジェクトが解放されないといった現象が発生することがあります。

特に遭遇しやすい原因が

  • shared_ptr同士の循環参照
  • ラムダが作る隠れた循環参照

の二つです。

本記事では、shared_ptrが抱える構造的な弱点と、これらのリークを起こさないためにどうすればよいかを紹介します。

前提知識

  • C++
  • スマートポインタのことを知っている
  • ラムダ式コールバック関数を知っている

サンプルコード

最も簡単な循環参照の例

CircularReference1.cpp
//-------------------------------------------------------------------------------
//! @file	CircularReference1.cpp
//! @brief	シェアードポインタの循環参照サンプル
//! @author つきの
//-------------------------------------------------------------------------------
#include <iostream>
#include <memory>
//-------------------------------------------------------------------------------
//! @class Object
//! @brief オブジェクトクラス
//! @note  メンバ変数でオブジェクトのポインタを管理する
//-------------------------------------------------------------------------------
class Object {
public:
	//-------------------------------------------------------------------------------
	//! @brief コンストラクタ
	//-------------------------------------------------------------------------------
	Object() {
		std::cout << "Objectのコンストラクタが呼ばれました。" << std::endl;
	};

	//-------------------------------------------------------------------------------
	//! @brief デストラクタ
	//-------------------------------------------------------------------------------
	virtual ~Object() {
		std::cout << "Objectのデストラクタが呼ばれました。" << std::endl;
	};

	//-------------------------------------------------------------------------------
	//! @brief オブジェクトのシェアードポインタを設定する関数
	//-------------------------------------------------------------------------------
	void setObjectPtr(const std::shared_ptr<Object>& object_ptr) {
		object_ptr_ = object_ptr;
	}

private:
	std::shared_ptr<Object> object_ptr_;//オブジェクトのシェアードポインタを持つ
};

//エントリポイント
int main() {
	//-------------------------------------------------------------------------------
	//オブジェクトのシェアードポインタを作成し、自分自身のシェアードポインタを設定
	//-------------------------------------------------------------------------------
	{
		std::shared_ptr<Object> object_ptr = std::make_shared<Object>();	// オブジェクトのシェアードポインタを作成
		object_ptr->setObjectPtr(object_ptr);								// 自分自身のシェアードポインタを設定
	} //スコープを抜けるときにデストラクタが呼ばれるはずだが・・・

	//プログラムの終了
	return 0;
}
result
Objectのコンストラクタが呼ばれました。

コンストラクタは呼ばれましたが、デストラクタが呼ばれませんでした。

これは、shared_ptrの仕組みに原因があります。

参照カウント

shared_ptrは、参照カウントという方式でポインタを管理しています。

生成されたときに1から始まり、特定の動作を行った際に参照カウンタが変化し、カウンタが0になったタイミングでデストラクタが呼ばれます。

参照カウントは、shared_ptrをコピーしたときに増え、shared_ptrがスコープを抜けたときや、shared_ptrに別の値を代入したときに減少します。

なぜ解放されなかったのか

CircularReference1.cppでは、なぜ解放されなかったのでしょうか。

CircularReference1.cpp
//エントリポイント
int main() {
	//-------------------------------------------------------------------------------
	//オブジェクトのシェアードポインタを作成し、自分自身のシェアードポインタを設定
	//-------------------------------------------------------------------------------
	{
		std::shared_ptr<Object> object_ptr = std::make_shared<Object>();	// ここでは参照カウントが1
		object_ptr->setObjectPtr(object_ptr);								// 関数内でコピーするので参照カウントが2に増える
	} // スコープを抜ける時にobject_ptrが破棄されて参照カウントが1になるが、object_ptr_が持つシェアードポインタがあるため参照カウントは0にならず、デストラクタが呼ばれない

	//プログラムの終了
	return 0;
}

スコープを抜けた際に外側のポインタ(object_ptr)は破棄されますが、メンバのobject_ptr_がのこっているため、参照カウントが0にならずに、デストラクタがよばれず、メンバのshared_ptr``がずっと破棄されずに、メモリリークが発生します。

回避方法

CircularReference1_2.cpp
//-------------------------------------------------------------------------------
//! @file	CircularReference1_2.cpp
//! @brief	シェアードポインタの循環参照回避サンプル
//! @author つきの
//-------------------------------------------------------------------------------
#include <iostream>
#include <memory>
//-------------------------------------------------------------------------------
//! @class Object
//! @brief オブジェクトクラス
//! @note  メンバ変数でオブジェクトのポインタを管理する
//-------------------------------------------------------------------------------
class Object {
public:
	//-------------------------------------------------------------------------------
	//! @brief コンストラクタ
	//-------------------------------------------------------------------------------
	Object() {
		std::cout << "Objectのコンストラクタが呼ばれました。" << std::endl;
	};

	//-------------------------------------------------------------------------------
	//! @brief デストラクタ
	//-------------------------------------------------------------------------------
	virtual ~Object() {
		std::cout << "Objectのデストラクタが呼ばれました。" << std::endl;
	};

	//-------------------------------------------------------------------------------
	//! @brief オブジェクトのウィークポインタを設定する関数
	//-------------------------------------------------------------------------------
	void setObjectPtr(const std::shared_ptr<Object>& object_ptr) {
		object_ptr_ = object_ptr;
	}

private:
	std::weak_ptr<Object> object_ptr_;//オブジェクトのウィークポインタを持つ
};

//エントリポイント
int main() {
	//-------------------------------------------------------------------------------
	// オブジェクトのシェアードポインタを作成し、自分自身のウィークポインタを設定
	//-------------------------------------------------------------------------------
	{
		std::shared_ptr<Object> object_ptr = std::make_shared<Object>();	// ここでは参照カウントが1
		object_ptr->setObjectPtr(object_ptr);								// weak_ptrは参照カウントを増やさない
	} // 外側ポインタの破棄のみで参照カウントが0になり、デストラクタが呼ばれる

	//プログラムの終了
	return 0;
}
result
Objectのコンストラクタが呼ばれました。
Objectのデストラクタが呼ばれました。

weak_ptr所有権を持たないスマートポインタで、参照カウントを増やしません。

そのため循環参照が発生せず、スコープを抜けたときに参照カウントが 0 になり、デストラクタが正しく呼ばれます。

お互いに参照しあってメモリリークするパターン

CircularReference2.cpp
//-------------------------------------------------------------------------------
//! @file	CircularReference2.cpp
//! @brief	シェアードポインタの循環参照サンプル
//! @author つきの
//-------------------------------------------------------------------------------
#include <iostream>
#include <memory>
//-------------------------------------------------------------------------------
//! @class Object
//! @brief オブジェクトクラス
//! @note  メンバ変数でオブジェクトのポインタを管理する
//-------------------------------------------------------------------------------
class Object {
public:
	//-------------------------------------------------------------------------------
	//! @brief コンストラクタ
	//-------------------------------------------------------------------------------
	Object() {
		std::cout << "Objectのコンストラクタが呼ばれました。" << std::endl;
	};

	//-------------------------------------------------------------------------------
	//! @brief デストラクタ
	//-------------------------------------------------------------------------------
	virtual ~Object() {
		std::cout << "Objectのデストラクタが呼ばれました。" << std::endl;
	};

	//-------------------------------------------------------------------------------
	//! @brief オブジェクトのシェアードポインタを設定する関数
	//-------------------------------------------------------------------------------
	void setObjectPtr(const std::shared_ptr<Object>& object_ptr) {
		object_ptr_ = object_ptr;
	}

private:
	std::shared_ptr<Object> object_ptr_;//オブジェクトのシェアードポインタを持つ
};

//エントリポイント
int main() {
	//-------------------------------------------------------------------------------
	//オブジェクトのシェアードポインタを二つ作成し、お互いのシェアードポインタを設定
	//-------------------------------------------------------------------------------
	{
		std::shared_ptr<Object> object_ptr1 = std::make_shared<Object>();
		std::shared_ptr<Object> object_ptr2 = std::make_shared<Object>();
		object_ptr1->setObjectPtr(object_ptr2);
		object_ptr2->setObjectPtr(object_ptr1);
	}

	//プログラムの終了
	return 0;
}
result
Objectのコンストラクタが呼ばれました。
Objectのコンストラクタが呼ばれました。

先ほど例に挙げた自己参照は特殊な例ですが、こちらの相互参照は良く見落としがちなメモリリークの原因です。
こちらも参照カウントが0にならないことでデストラクタが呼ばれません。

回避方法

CircularReference2_2.cpp
//-------------------------------------------------------------------------------
//! @file	CircularReference2_2.cpp
//! @brief	シェアードポインタの循環参照回避サンプル
//! @author つきの
//-------------------------------------------------------------------------------
#include <iostream>
#include <memory>
//-------------------------------------------------------------------------------
//! @class Object
//! @brief オブジェクトクラス
//! @note  メンバ変数でオブジェクトのポインタを管理する
//-------------------------------------------------------------------------------
class Object {
public:
	//-------------------------------------------------------------------------------
	//! @brief コンストラクタ
	//-------------------------------------------------------------------------------
	Object() {
		std::cout << "Objectのコンストラクタが呼ばれました。" << std::endl;
	};

	//-------------------------------------------------------------------------------
	//! @brief デストラクタ
	//-------------------------------------------------------------------------------
	virtual ~Object() {
		std::cout << "Objectのデストラクタが呼ばれました。" << std::endl;
	};

	//-------------------------------------------------------------------------------
	//! @brief オブジェクトのウィークポインタを設定する関数
	//-------------------------------------------------------------------------------
	void setObjectPtr(const std::shared_ptr<Object>& object_ptr) {
		object_ptr_ = object_ptr;
	}

private:
	std::weak_ptr<Object> object_ptr_;//オブジェクトのウィークポインタを持つ
};

//エントリポイント
int main() {
	//-------------------------------------------------------------------------------
	//オブジェクトのシェアードポインタを二つ作成し、お互いのウィークポインタを設定
	//-------------------------------------------------------------------------------
	{
		std::shared_ptr<Object> object_ptr1 = std::make_shared<Object>();
		std::shared_ptr<Object> object_ptr2 = std::make_shared<Object>();
		object_ptr1->setObjectPtr(object_ptr2);
		object_ptr2->setObjectPtr(object_ptr1);
	}

	//プログラムの終了
	return 0;
}
result
Objectのコンストラクタが呼ばれました。
Objectのコンストラクタが呼ばれました。
Objectのデストラクタが呼ばれました。
Objectのデストラクタが呼ばれました。

こちらも、オブジェクト内部に所有権を持たずに、参照したい場合はweak_ptrを持つという風にすることで、メモリリークを回避することが出来ます。

ラムダ式でのキャプチャによるメモリリーク

CircularReference3.cpp
//-------------------------------------------------------------------------------
//! @file	CircularReference3.cpp
//! @brief	ラムダ式のキャプチャによるシェアードポインタの循環参照サンプル
//! @author つきの
//-------------------------------------------------------------------------------
#include <iostream>
#include <memory>
#include <functional>
//-------------------------------------------------------------------------------
//! @class Object
//! @brief オブジェクトクラス
//! @note  メンバ変数でオブジェクトのポインタを管理する
//-------------------------------------------------------------------------------
class Object {
public:
	//-------------------------------------------------------------------------------
	//! @brief コンストラクタ
	//-------------------------------------------------------------------------------
	Object() {
		std::cout << "Objectのコンストラクタが呼ばれました。" << std::endl;
	};

	//-------------------------------------------------------------------------------
	//! @brief デストラクタ
	//-------------------------------------------------------------------------------
	virtual ~Object() {
		std::cout << "Objectのデストラクタが呼ばれました。" << std::endl;
	};

	//-------------------------------------------------------------------------------
	//! @brief コールバック関数の設定を行う関数
	//! @param callback	[in] コールバック関数
	//-------------------------------------------------------------------------------
	void setCallback(const std::function<void()>& callback) {
		callback_ = callback;
	}

	//-------------------------------------------------------------------------------
	//! @brief コールバック関数の実行を行う関数
	//! @note コールバック関数が設定されていない場合は何もしない
	//-------------------------------------------------------------------------------
	void executeCallback() {
		if (callback_) {
			callback_();
		}
	}

	//-------------------------------------------------------------------------------
	//! @brief メッセージを表示するだけの関数
	//! @note  サンプルのためにこれをラムダで呼び出したいとする
	//-------------------------------------------------------------------------------
	void showMessage() {
		std::cout << "いいねとフォローよろしくね" << std::endl;
	}

private:
	std::function<void()> callback_ = nullptr;	//!< コールバック関数
};

//エントリポイント
int main() {
	//-------------------------------------------------------------------------------
	// オブジェクトを作成して、コールバックからshowMessageを呼び出す
	//-------------------------------------------------------------------------------
	{
		std::shared_ptr<Object> obj = std::make_shared<Object>(); // Objectのインスタンスを作成
		// コールバック関数を書く(コピーで自分自身をキャプチャするので、参照カウンタが2に)
		auto callback = [obj]() {
			obj->showMessage();
			};
		obj->setCallback(callback);	// コールバック関数を設定する
		obj->executeCallback();		// 便宜上実行する
	} //参照カウンタが0にならないので解放されない

	//プログラムの終了
	return 0;
}
result
Objectのコンストラクタが呼ばれました。
いいねとフォローよろしくね

このパターンが厄介で、実はラムダ式コピーキャプチャ参照カウンタが増えてしまうためメモリリークが起きやすいのですが、意外と盲点になりがちだったりします。

私もこれを知らずに開発を進めて、終盤でえらい目にあったことがあります。

回避方法

CircularReference3_2
//-------------------------------------------------------------------------------
//! @file	CircularReference3_2.cpp
//! @brief	ラムダ式のキャプチャによるシェアードポインタの循環参照サンプル
//! @author つきの
//-------------------------------------------------------------------------------
#include <iostream>
#include <memory>
#include <functional>
//-------------------------------------------------------------------------------
//! @class Object
//! @brief オブジェクトクラス
//! @note  メンバ変数でオブジェクトのポインタを管理する
//-------------------------------------------------------------------------------
class Object {
public:
	//-------------------------------------------------------------------------------
	//! @brief コンストラクタ
	//-------------------------------------------------------------------------------
	Object() {
		std::cout << "Objectのコンストラクタが呼ばれました。" << std::endl;
	};

	//-------------------------------------------------------------------------------
	//! @brief デストラクタ
	//-------------------------------------------------------------------------------
	virtual ~Object() {
		std::cout << "Objectのデストラクタが呼ばれました。" << std::endl;
	};

	//-------------------------------------------------------------------------------
	//! @brief コールバック関数の設定を行う関数
	//! @param callback	[in] コールバック関数
	//-------------------------------------------------------------------------------
	void setCallback(const std::function<void()>& callback) {
		callback_ = callback;
	}

	//-------------------------------------------------------------------------------
	//! @brief コールバック関数の実行を行う関数
	//! @note コールバック関数が設定されていない場合は何もしない
	//-------------------------------------------------------------------------------
	void executeCallback() {
		if (callback_) {
			callback_();
		}
	}

	//-------------------------------------------------------------------------------
	//! @brief メッセージを表示するだけの関数
	//! @note  サンプルのためにこれをラムダで呼び出したいとする
	//-------------------------------------------------------------------------------
	void showMessage() {
		std::cout << "いいねとフォローよろしくね" << std::endl;
	}

private:
	std::function<void()> callback_ = nullptr;	//!< コールバック関数
};

//エントリポイント
int main() {
	//-------------------------------------------------------------------------------
	// オブジェクトを作成して、コールバックからshowMessageを呼び出す
	//-------------------------------------------------------------------------------
	{
		std::shared_ptr<Object> obj = std::make_shared<Object>(); // Objectのインスタンスを作成
		std::weak_ptr<Object> obj_weak = obj;
		// コールバック関数を書く(ウィークポインタをキャプチャするので参照カウンタが増えない)
		auto callback = [obj_weak]() {
			if (auto obj = obj_weak.lock()) {
				obj->showMessage();
			}
			};
		obj->setCallback(callback);	// コールバック関数を設定する
		obj->executeCallback();		// 便宜上実行する
	} //参照カウンタが0になるので適切に解放される

	//プログラムの終了
	return 0;
}
result
Objectのコンストラクタが呼ばれました。
いいねとフォローよろしくね
Objectのデストラクタが呼ばれました。

こちらも、weak_ptrでキャプチャを行うことで、循環参照の問題が解決されます。

総括

  • shared_ptrでは、参照カウンタが0になったときにポインタが解放される
  • 循環参照などで参照カウンタが0にならない場合、メモリリークを起こしてしまう
  • 所有したくないが参照を行いたいというケースでは、shared_ptrではなく、weak_ptrを使用するのが適切
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?