並ゲームプログラマの書き捨て場

ゲームプログラマやってる私の日記、雑記、備忘録、公開場所

それでもdecltypeの挙動は謎である

貴方はこういう人ですね?djannです。


depotには現在with_state_valueというテンプレートが存在する。boostに存在し、C++17で標準入りされるといわれているoptionalの表面的な概念を取り入れたもので、指定した型のオブジェクトと、その有効状態を保持するものだ。

depot::with_state_value<int> val;

if ( val ){
	// unreachable.
	std::cout << "available : " << *val << std::endl;
} else {
	std::cout << "unavailable." << std::endl;
}

val = 10;

if ( val ){
	std::cout << "available : " << *val << std::endl;
} else {
	// unreachable.
	std::cout << "unavailable." << std::endl;
}

// explicit destroy.
val = depot::null_value();

if ( val ){
	// unreachable.
	std::cout << "available : " << *val << std::endl;
} else {
	std::cout << "unavailable." << std::endl;
}

// emplace object.
val.emplace( 25 );

if ( val ){
	std::cout << "available : " << *val << std::endl;
} else {
	// unreachable.
	std::cout << "unavailable." << std::endl;
}

実行結果は以下のようになる。

unavailable.
available : 10
unavailable.
available : 25


このwith_state_valueを用いる際に、例えばunavailable状態のフローで間違えてvalを用いてしまうこともあるかもしれない。もちろん「そんなコードを書く方が悪い」と言ってしまえばそれまでであるし、間違えてunavailableなwith_state_valueを用いた際には即停止するようになっているのですぐに気付くだろう。

だがそれ以前に、間違いを起こせない書き方をしたいという場合もある。そんな場合に用いるフリーテンプレート関数を用意しようとしたところ、「Visual Studio上で」decltypeの挙動が想定通りに動かずに現在調査中であり、その解決法を読者に教授いただきたくこの記事を書き始めた次第である。


想定仕様

想定している使用法としては、以下の通りである。

  • 第一引数にはwith_state_value&を受け取る
  • 第二引数には関数(関数オブジェクト、ラムダ)を受け取る
    • 第二引数がwith_state_valueのテンプレート引数を1つ受け取るのならばそれはavailable状態に行う処理
    • 第二引数の関数が引数を受け取らないのならばそれはunavaiable状態に行う処理
  • 戻り値は渡された関数準拠とし、if式のような形で記述できることとする
  • 三引数版があり、こちらはavaiable状態、unavaiable状態両方の関数を受け取る
    • 第二引数はavailable用、第三引数はunavaiable用とし、それぞれは前述の仕様に相当する
    • この場合、両関数の戻り値の型が異なる場合はエラーとする

コードとしては以下の通りとなる。

depot::with_state_value<int> val;

// no running.
depot::match( val, []( int &n ){
	std::cout << "available : " << n << std::endl;
});
// running.
depot::match( val, [](){
	std::cout << "unavaiable." << std::endl;
});
val.emplace( 10 );
// three arguments.
depot::match( val, []( int &n ){
	std::cout << "available : " << n << std::endl;
}, [](){
	std::cout << "unavaiable." << std::endl;
});

これの実現の為、以下のようなコードを書いたところ、Clangでは正常に働き、Visual Studioでは(2012, 2013, 2015でテスト)、使用しようとすらしていない段階で引数無し版を受け取る関数で「定義済みの関数が再定義された」とのエラーが出てしまう。

template <class vType, class FuncType>
auto match( depot::with_state_value<vType> &val, FuncType fun ) -> decltype(fun(*val)) {
	if ( val ){
		return fun( *val );
	}
	return decltype(fun(*val))();	// default value.
}
template <class vType, class FuncType>
auto match( depot::with_state_value<vType> &val, FuncType fun ) -> decltype(fun()) {
	if ( !val ){
		return fun();
	}
	return decltype(fun())();	// default value.
}
template <class vType, class ThenFunc, class ElseFunc>
auto match( depot::with_state_value<vType> &val, ThenFunc fun1, ElseFunc fun2 )
	-> typename enable_if<is_same<decltype(fun1(*val)), decltype(fun2())>::value, decltype(fun1(*val))>::type
{
	if ( val ){
		return fun1( *val );
	} else {
		return fun2();
	}
}

インスタンス化のタイミングでdecltype(fun())が評価できず、その結果オーバーロード候補から外され1引数版と区別され呼び出されると思っていたのだが、その認識は間違っていたのだろうか?
ちなみにdecltypeを使わずenable_if等でオーバーロード候補から外している他の関数テンプレートの場合は、Visual Studio上でもその想定通りの挙動を行ってくれている。

知りたいのは、この挙動が規格上はどうなのかという部分と、Visual Studio上でどう回避すればよいのかという部分である。

  • 戻り値の型をFuncTypeより取得する
  • 引数無し、1引数で振り分ける

というのが必要となる。

追記

少なくともVisual Studio 2012でdecltype(&lambda::operator())によってRetType(Args...)のシグネチャが得られるようだ(Clangでは駄目。規格上も駄目な感じがする)。なのでVisual Studio上では窓口の関数を一つだけ定義し、そこのみでdecltypeを用いて内部で呼び出す関数で従来のメタ関数を用いて判別を行う。という形で実装出来そうだ。

追記その2

前述の手法を用いて、Visual Studio 2012上で動作するmatch()関数の作成に成功した(現状lambdaでしか試していないし、関数ポインタを渡した際にはおそらくコンパイルエラーになる、というかコンパイラの内部エラーが発生した)。なお、このコードはClangではそもそもエラーとなる。おそらくdecltype(&FuncType::operator())と取得しているところが原因と思われる。そもそもClangの場合プリプロセッサで分岐させ、最初に思いついたコードを働かせれば問題ない。

// fetch a signature.
namespace match_use_functions {
	// result type for lambda in visual studio 2012.
	template <class vType>
	struct result_of;
	template <class RetType, class classType>
	struct result_of<RetType (classType::*)()const>{
		typedef RetType type;
	};
	template <class RetType, class classType, class ArgType>
	struct result_of<RetType (classType::*)(ArgType)const>{
		typedef RetType type;
	};

	// zero or one argument condition.
	template <class vType>
	struct has_argument : public depot::false_type {};
	template <class RetType, class classType, class ArgType>
	struct has_argument<RetType (classType::*)(ArgType)const> : public depot::true_type {
		typedef ArgType type;	// not used.
	};
}	// namespace match_use_functions.

template <class vType, class FuncType>
struct match_t {
	template <class SigType>
	static auto invoke( depot::with_state_value<vType> &value, FuncType &func )
		-> typename depot::enable_if<depot::match_use_functions::has_argument<SigType>, typename match_use_functions::result_of<SigType>::type>::type
	{
		if ( value ){
			return func( *value );
		}
		return typename match_use_functions::result_of<SigType>::type();
	}
	template <class SigType>
	static auto invoke( depot::with_state_value<vType> &value, FuncType &func )
		-> typename depot::disable_if<depot::match_use_functions::has_argument<SigType>, typename match_use_functions::result_of<SigType>::type>::type
	{
		if ( !value ){
			return func();
		}
		return typename match_use_functions::result_of<SigType>::type();
	}
};

//
template <class vType, class FuncType>
auto match( depot::with_state_value<vType> &value, FuncType func )
	-> decltype(depot::match_t<vType,FuncType>::template invoke<decltype(&FuncType::operator())>( value, func ))
{
	return depot::match_t<vType,FuncType>::template invoke<decltype(&FuncType::operator())>( value, func );
}
template <class vType, class ThenFunc, class ElseFunc>
auto match( depot::with_state_value<vType> &val, ThenFunc fun1, ElseFunc fun2 )
	-> typename depot::enable_if<depot::is_same<decltype(fun1(*val)), decltype(fun2())>, decltype(fun1(*val))>::type
{
	if ( val ){
		return fun1( *val );
	} else {
		return fun2();
	}
}

※depot::enable_ifはstd::enable_ifと違いtrue_type, false_typeを受け取り、第二引数がvoid型でもtypeを持つもの。

追記その3

どうやらこれはSFINAE式?と呼ばれるものに当たるようで、Visual Studio上では明確にそちらの対応が行われていないとアナウンスされているようだ。sizeofやdecltypeを用いた「式」によるSFINAEはVisual Studio上では動かない。と事前認識を持って、以降は取り掛かる必要があるようだ。

なお、decltypeを用いてlambda::operator()のシグネチャを取得する事も、もちろん規格上認められていないので、完全に「現存するバージョンの」Visual Studio専用workaroundとなる。このようなコードは出来るだけ書くべきではない。