忍者ブログ

VIEWブログ

tk-xleaderのブログ。C++などプログラミングの話題が中心です。

【C++2a】Customization pointについて

×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。

【C++2a】Customization pointについて

C++2a規格では、標準ライブラリに関してCustomization pointという概念が導入されます。Customization pointとは、標準ライブラリの関数テンプレートの中で、ユーザー定義の型に対する特殊な定義を付け加えることを前提としたもののことです。C++2aでは、標準ライブラリの関数テンプレートについて、カスタム定義をすることができるものを限定し、その上で、カスタム定義の方法についても、ADLを前提としたものに限るというルールに変更されます。

変更点のポイント

C++17(N4659)までは、std名前空間内のテンプレートに対して、一定の場合を除いて完全特殊化/部分特殊化を定義することができると決められています。関数テンプレートの場合、部分特殊化を定義することができない1ため、関数テンプレートについては、完全特殊化の場合のみが許されていることになります。
これが、C++2a(n4762)では、ユーザー定義型に対する特殊化を定義することができるのはクラステンプレートのみとするように変更されます。つまり、std名前空間内の関数テンプレートについては、完全特殊化も含めて特殊化をすることが禁止されます。この変更の理由は、"Thou Shalt Not Specialize std Function Templates!"(提案文書p0551r3)によれば、関数テンプレートの特殊化はオーバーロード解決に際して考慮されないため、関数テンプレートの特殊化はユーザーの意図とは異なる結果をもたらす可能性があります。例えば、次のようなコードがあるとします。
/*code start*/
/*in library code.*/ #include<iostream> namespace library{ template<typename T> void func(const T&){} template<typename T> struct X{}; template<typename T> void func(const X<T>&){ std::cout << "template<typename T> void func(const X<T>&);" << std::endl; }// ※1 } /*user code*/ namespace user{ class Y{}; } namespace library{ template<> void func<X<user::Y>>(const X<user::Y>&){ std::cout << "template<> void func<X<Y>>(const X<Y>&);" <<std::endl; }// ※2 } int main(){ using namespace library; func(library::X<user::Y>{});//出力は? }
//end
一般的に、ユーザーは※2が呼ばれることを期待するでしょう。ところが、このコードで呼ばれるfuncは、※2ではなく※1となります。こういう問題があるため、std名前空間内の関数テンプレートの特殊化は禁止すべきだということになったわけです。結果として、Customization pointのカスタム化は、ユーザー定義型と同じ名前空間内に同名の関数を定義して、標準ライブラリはそれを非修飾名で呼び出してADLで解決させるという方法に落ち着くということになります。
C++2aでは、std::swap, std::begin, std::endなどがCustomization pointとして指定されることになっています。
≪追記≫
※2を※1の特殊化として実装する場合、次のように定義するのが正解です。

namespace library{
	template<>
	void func<>(const X<user::Y>&){
		std::cout << "template<> void func<X<Y>>(const X<Y>&);" <<std::endl;
	}
}

Customization point object

さらに、Customization point "object"という概念も導入されています2。 これは、Customization pointをユーザーコードから呼び出す場合に、usingを使わなくてもユーザー定義のCustomization pointが呼び出されるようにするための機構です。実現方法は異なります3が、boost::swapと同様の目的があります。
Customization point objectは、その名の通りオブジェクトとして実装されます。"Suggested Design for Customization Points"(N4381)と提唱者のEric Niebler氏のブログエントリで詳細が述べられていますが、仮に、std::swapをCustomization point objectとして実装した場合、次のような実装になります。
//start code
namespace std{
	namespace __swap_details{
		template<typename T>
		inline typename std::enable_if<std::is_move_constructible<T>::value&&std::is_move_assignable<T>::value>::type 
		swap(T& obj1, T& obj2)noexcept(std::is_nothrow_move_constructible<T>::value&&std::is_nothrow_move_assignable<T>::value){
			T temp(std::move(obj1));
			obj1 = std::move(obj2);
			obj2 = std::move(temp);
		} //※1
		struct _swap_functor{
			template<typename T, typename U>
			auto operator()(T& obj1, U& obj2)const noexcept(noexcept(swap(obj1, obj2)))->decltype(static_cast<void>(swap(obj1, obj2))){
				swap(obj1, obj2);
			} //※2
		};
	}
	inline constexpr __swap_details::_swap_functor swap{}; //※3

/* // Eric Niebler氏のエントリでは、ODR違反の回避のため※3は次のような実装になっている。 // C++17にはinline変数が導入されたので、変数swapはinline変数として定義できる。
template<typename T> struct __static_valuable{ static constexpr T value; }; template<typename T> constexpr T __static_valuable<T>::value; namespace{ constexpr auto const& swap = __static_valuable<__swap_details::_swap_functor>::value; } */ } //end code
まず、※1のように、swap関数のデフォルトの実装を用意します。肝は、std名前空間とは別の名前空間にこれを定義することです。そして、これと同じ名前空間内に、swap関数を非修飾名で呼び出すような関数オブジェクト_swap_functorを定義します。_swap_functorは、ユーザー定義のswapをADLによって呼び出すことができます。最後に、std名前空間にswapという名前で_swap_functor型の変数を定義します。こうすることで、std::swapとして関数テンプレートと同様のSyntaxで呼び出すことができます。

このように定義したswapは、std::swapと修飾名で呼び出した場合であっても、ADLによってユーザー定義のswapが呼び出されます。そのため、using std::swap; や、using namespace std; としてswapを非修飾名で呼び出す方法(探索先にstd名前空間を加えてADLを意図的に引き起こす手段)でなくても、それと同等の効果が得られるというわけです。
ただし、この方法を取った場合、ユーザーがデフォルトのswap関数に限定して呼び出す方法がなくなります。もちろん、それはライブラリ実装者の意図するところではあるのですが。

ユーザーコードへの応用

仮に、boost::swapをCustomization point objectと同様の手法を利用して実装すると、次のような実装になります。
//start code
namespace boost{ namespace _swap_impl{ using std::swap; struct _swap_functor{ template<typename T, typename U> void operator(T& obj1, U& obj2)const noexcept(noexcept(swap(obj1, obj2))){ swap(obj1, obj2); } }; } inline constexpr _swap_impl::_swap_functor swap{}; //since C++17. } //end code
std::swapを定義する場合と異なるのは、デフォルトの実装として用意されたswap関数が、修飾名でstd::swapを呼び出すようにしたことです。もう一つ、SFINAEによる限定を一切行っていないことです。その理由としては、標準規格上、swap関数テンプレートは、引数の型がswap関数のテンプレート引数の要件を満たさない場合(つまり、ムーブ構築とムーブ代入のどちらか出来ない型である場合)はオーバーロード解決から外すことが要求されますが、boost::swapについては、独自のswapが定義されておらず、かつstd::swapが呼び出せない型についてはエラーにしてしまえばいいので、一々面倒なenable_ifを書く必要はないだろう思ったからです。

Appendix: ADLとtemplateライブラリ

ここまでの話は、ADLというC++特有の名前探索の機構を前提とした話です。そして、残念なことに多くのC++erはADLについてあまり好意的ではありません。なぜなら、ADLでは予測しなかった関数が呼ばれる可能性があるからです。C++erは、ADLに対して十分に注意を払う必要があります。ここでは、templateライブラリを作成するときと、それを利用するときについて検討することにしましょう。
templateライブラリを作成する側は、テンプレート内で関数を呼び出す場合に、呼び出す関数がライブラリユーザーによるカスタマイズを前提とした関数なのか、そうでないのかを分ける必要があります。前者の場合は非修飾名で呼び出しても(あるいは、Customization point objectとしてしまっても)問題はないと思います。呼び出す関数が後者の場合、ADLが発生しないような方法で呼び出すようにしましょう。ADLを発生させないためには、関数を修飾名で呼び出すという方法があります。
また、カスタマイズを前提とする関数を呼び出す場合、同一名前空間内に、引数の型が取りうる、最も一般的なテンプレート仮引数を取る関数テンプレートをデフォルトの実装として定義するべきだろうと思います。例えば、
//start code
namespace library{
	template<typename T, typename U, typename V>
	void func(const T& t, U* u, const std::vector<V>& v){
		inner_func1(t);
		inner_func2(u);
		inner_func3(v);
	}
}
//end code
という関数テンプレートを書くとしたら、
//start code
namespace library{ template<typename T> void inner_func1(const T& t){/*...*/} template<typename T> void inner_func2(T* t){/*...*/} template<typename T> void inner_func3(const std::vector<T>& t){/*...*/} } //end code
という関数を定義するべきだということです。その理由は、カスタマイズされた関数は、当然のことながらテンプレート引数よりも特殊な型になっているはずなので、デフォルトの実装のような一般的に過ぎる関数テンプレートであるべきではないからです。仮に、全ての型についてカスタマイズすることを要求する(つまり、デフォルトの実装を用意しない)場合、delete定義としておけば事足ります。

ライブラリのユーザーは、ユーザー定義型を定義するときに、不用意にADLが発生しないようにしましょう(もちろん、ADLで呼ばれることを意図した関数については別です)。ADLを妨害する方法としては、ADL firewallという手法があります。その肝はユーザー定義型とフリー関数を異なる名前空間で定義することです。一般的には、ネストした名前空間を用意して、その中でユーザー定義型を定義して、関数を定義した名前空間でusing宣言、あるいはtypedef宣言するという方法を取ります。
//start code
namespace ns{
	namespace nested{
		struct X{};
	}
	
	// 次のどれでもよい。どの方法でも、ns::Xでns::nested::Xを指すことができる。
	using nested::X;    //(1)
	typedef nested::X X;//(2)
	using namespace X;  //(3)
	
	void func(X x){}
}
//end code
一方、関数の方をネストされた名前空間に入れてもよいですが、この場合はusing宣言(1の方法)ではだめです。なぜなら、using宣言によって宣言された関数は、ADLの対象になるからです4

  1. 関数テンプレートについて、template<typename T> void swap<foo<T>>(foo<T>&, foo<T>&){/*...*/} みたいな定義をすることはできない。template<typename T> void swap(foo<T>&, foo<T>&){/*...*/} とした場合、これは部分特殊化ではなくて関数テンプレートのオーバーロードとなる。
  2. 現在の最新draft(N4659)の段階では、Customization point objectに指定されたものは見当たらない。これは、N4381で検討されているが、既存のCustomization pointにこれを適用すると、Customization pointを修飾名で呼び出しているコードを破壊的に変更することになるからである(Customization pointを修飾名で呼び出している場合、ユーザー定義の関数は呼ばれず、std名前空間内の関数が呼ばれる)。
  3. N4659では、Customization point objectの型はliteral class typeでなければならないとされているため。一方で、boost::swapを関数オブジェクトにしてしまった場合、boost名前空間にswap関数を定義することができなくなってしまうため、boost::swapは関数として定義せざるを得ない。ところで、boost::swap自身は、std::swapを非修飾名で呼び出せる文脈においては、boost名前空間内で定義されている型に対してADLによって呼ばれることはない。なぜなら、boost::swapは、テンプレート引数を2つ取り、テンプレート引数が1つであるstd::swapよりもオーバーロード解決について劣後するためである。
  4. これを利用すれば、他の名前空間で定義された関数をADLの対象にすることができる。例えば、std::rel_ops名前空間内の演算子関数については、本来ならばこの方法で使うべきではないかと思われる(もっとも、std::rel_ops名前空間内の比較演算子については、C++2aでdeprecatedとなることが決まっている)。
    PR

    コメント

    プロフィール

    HN:
    tk-xleader
    Webサイト:
    性別:
    男性

    カテゴリー

    P R