1. サイトトップ
  2. ブログ
  3. C++
  4. 【C++】C++のstd::bind、std::functionでのムーブコンストラクタ、コピーコンストラクタ、デストラクタの呼び出しを確認してみる

【C++】C++のstd::bind、std::functionでのムーブコンストラクタ、コピーコンストラクタ、デストラクタの呼び出しを確認してみる

こんにちは、制作部プログラマの青柳です。
弊社のブログですが代表の堂前以外にも書かせて頂く事になりました。
なにがしかの形でお役に立つ事があれば幸いです。

さて、UE4やUnityなど商用エンジンが当たり前となっていますがC++を使用する事もまだまだあるかと思います。
またC++自体も進化し、さらに高機能になっていっていますので、今回は改めて表題の件に関して実際に実行してみて確認していきたいと思います。

環境はWindows10、VS2017を使用します。

std::function, std::bindに関して

std::functionとは
C++日本語リファレンス std::functionを見ると下記の様にあります。

functionクラステンプレートは、パラメータの型リストArgTypes…、戻り値の型Rに合致する、あらゆる関数ポインタ、関数オブジェクト、メンバ関数ポインタ、メンバ変数ポインタを保持できるクラスである。

C++日本語リファレンス std::function

ざっくりとすると関数を保持して実行できる、という事のようです。
std::bindの方は詳しくは置いておいて、、、
任意の引数をstd::function<void()>に紐づけてくれたりする便利な機能を提供してくれます。
void Hoge(int)

std::function<void()> fugaFunc;
と宣言されたfugaFuncにて
fugaFunc();
でHogeを実行出来たりするという事ですね。

ただ内部がどうなっているのか気になるので、手始めにバインドされた引数の寿命を調べて動作を探りたいと思います。

プログラム概要

class MyBindTarget
{
public:
	//コンストラクタ
	MyBindTarget() {
		id_ = refCount_;
		++refCount_;
		
		std::cout << "MyBindTargetのコンストラクタ呼び出し id: " << id_ << std::endl;
	}
	//デストラクタ
	~MyBindTarget() { 
		std::cout << "MyBindTargetのデストラクタ呼び出し id: " << id_ << std::endl;
	}
	//コピーコンストラクタ
	MyBindTarget(const MyBindTarget& value) { 
		id_ = refCount_;
		++refCount_;

		message_ = value.message_;
		std::cout << "MyBindTargetのコピーコンストラクタ呼び出し id: " << id_ << " コピー元の id: " << value.id_ << std::endl;
	}
	//ムーブコンストラクタ
	MyBindTarget(MyBindTarget&& value) { 
		id_ = refCount_;
		++refCount_;

		message_ = std::move(value.message_);
		std::cout << "MyBindTargetのムーブコンストラクタ呼び出し id: " << id_ << " ムーブ元の id:  " << value.id_ << std::endl;
	}
    ...
int main(int argc, char *args[])
{
	std::cout << "プログラムスタート" << std::endl << std::endl;
	{
		std::cout << "ここでstd::functionの変数 tempFuncを作成" << std::endl << std::endl;
		std::function<void()> tempFunc;
		{
			std::cout << "MyBindTarget型の変数 bindTargetを作成" << std::endl << std::endl;
			MyBindTarget bindTarget;

			//ラムダ式の宣言
			auto testLamd = [](MyBindTarget& _bindTarget){//std::weak_ptr<MyBindTarget> _bindTarget) {
				std::cout << "ラムダ式のメソッドを実行 MyBindTargetのメッセージ:  " << _bindTarget.GetMessage().c_str() << std::endl;
			};

			std::cout << std::endl;

			std::cout << "std::bindの呼び出し" << std::endl;
			tempFunc = std::bind(testLamd, bindTarget);

			std::cout << std::endl;

			std::cout << "std::functionのtempFuncの実行" << std::endl;
			tempFunc();

			std::cout << std::endl;

			//試しにローカル変数のメッセージを変更してみる
			std::cout << "ローカル変数のbindTargetは: " << bindTarget.GetMessage().c_str() << std::endl;
			bindTarget.SetMessage( " : Changed Message......" );
			std::cout << "変更されたbindTargetのMessageは: " << bindTarget.GetMessage().c_str() << std::endl;
		}
		std::cout << "localMyBindTarget Destroy" << std::endl << std::endl;

		std::cout << "std::functionのtempFuncの実行" << std::endl;
		tempFunc();
	}
	std::cout << "std::function型変数のtempFuncが破棄" << std::endl << std::endl;
	std::cout << "プログラムエンド、ここまでですべての変数は破棄される " << std::endl;
}

上記のようなプログラムを使ってMyBindTargetのコピーコンストラクタ、ムーブコンストラクタをログ出力させます。

結果

という事で結果は以下になりました。

プログラムスタート

ここでstd::functionの変数 tempFuncを作成

MyBindTarget型の変数 bindTargetを作成

MyBindTargetのコンストラクタ呼び出し id: 0

std::bindの呼び出し
MyBindTargetのコピーコンストラクタ呼び出し id: 1 コピー元の id: 0
MyBindTargetのムーブコンストラクタ呼び出し id: 2 ムーブ元の id:  1
MyBindTargetのムーブコンストラクタ呼び出し id: 3 ムーブ元の id:  2
MyBindTargetのデストラクタ呼び出し id: 2
MyBindTargetのデストラクタ呼び出し id: 1

std::functionのtempFuncの実行
ラムダ式のメソッドを実行 MyBindTargetのメッセージ:  Idは 3 メッセージは Message...

ローカル変数のbindTargetは: Idは 0 メッセージは Message...
変更されたbindTargetのMessageは: Idは 0 メッセージは  : Changed Message......
MyBindTargetのデストラクタ呼び出し id: 0
MyBindTarget型の変数 bindTargetが破棄

std::functionのtempFuncの実行
ラムダ式のメソッドを実行 MyBindTargetのメッセージ:  Idは 3 メッセージは Message...
MyBindTargetのデストラクタ呼び出し id: 3
std::function型変数のtempFuncが破棄

プログラムエンド、ここまでですべての変数は破棄される

std::bindの呼び出しの時にコピーされたMyBindTargetがtempFuncにムーブで保持されているんですね。
tempFunc実行の際はそちらを参照していると、id:0番が破棄された後でもtempFuncは正常に実行できています。

ラムダ式の引数をコピーへ変更

次はラムダ式の引数をキャプチャでなくしてみます。

auto testLamd = [](MyBindTarget _bindTarget) {
				std::cout << "execute Lamd " << _bindTarget.GetMessage().c_str() << std::endl;
			};

実行結果の変わったところ、tempFuncの実行結果部分だけ抜き出してみます。

std::functionのtempFuncの実行
MyBindTargetのコピーコンストラクタ呼び出し id: 4 コピー元の id: 3
ラムダ式のメソッドを実行 MyBindTargetのメッセージ:  Idは 4 メッセージは Message...
MyBindTargetのデストラクタ呼び出し id: 4
std::functionのtempFuncの実行
MyBindTargetのコピーコンストラクタ呼び出し id: 5 コピー元の id: 3
ラムダ式のメソッドを実行 MyBindTargetのメッセージ:  Idは 5 メッセージは Message...
MyBindTargetのデストラクタ呼び出し id: 5

キャプチャでない場合は実行の前にコピーしてるんですね。

引数をポインタへ変更

ちなみにポインタを引数にした場合は、当然かもしれませんがコピーコンストラクタ、ムーブコンストラクタは呼ばれません。
今のプログラムだと最後のtempFunc();で既に破棄されているMyBindTargetにアクセスして死にそうですが今回の環境では死にませんでした。

引数を弱参照へ変更

不正なアクセスのままだと気持ちが悪いので死んだことを検知出来るよう
ラムダ式を以下の様にWeakPtrに変更します。

auto testLamd = [](std::weak_ptr<MyBindTarget> _bindTarget) {
				if (!_bindTarget.expired()) {
					std::cout << "ラムダ式のメソッドを実行 MyBindTargetのメッセージ:  " << ( _bindTarget.lock() )->GetMessage().c_str() << std::endl;
				}
				else
				{
					std::cout << "既に破壊済みの引数です" << std::endl;
				}
			};

弱参照にした場合の結果は以下になります。

プログラムスタート

ここでstd::functionの変数 tempFuncを作成

MyBindTarget型の変数 bindTargetを作成

MyBindTargetのコンストラクタ呼び出し id: 0

std::bindの呼び出し

std::functionのtempFuncの実行
ラムダ式のメソッドを実行 MyBindTargetのメッセージ:  Idは 0 メッセージは Message...

ローカル変数のbindTargetの Messageは: Idは 0 メッセージは Message...
変更されたbindTargetのMessageは: Idは 0 メッセージは  : Changed Message......
MyBindTargetのデストラクタ呼び出し id: 0
MyBindTarget型の変数 bindTargetが破棄

std::functionのtempFuncの実行
既に破壊済みの引数です
std::function型変数のtempFuncが破棄

プログラムエンド、ここまでですべての変数は破棄される

終わりに

検証は以上になります。
std::bindでバインドされた引数の寿命が気になって調べてみましたが、std::bindの呼び出しでコピーされているのだから考えてみれば当たり前の結果でした。
(※参照渡しにはstd::refを使用するんですね)
std::bind、std::function完全に理解した、のでは全然ありませんがこの場合に中で何が行われてそうか少しだけ分かった気がしてきますね。


【免責事項】

本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。