C++ の名前解決 ADL その2

はじめに

C++20 の前の基礎知識として、以前 C++ の名前解決 でも紹介した ADL (Argument-dependent name lookup) に関連した話題を少し紹介してみます



■蛇足

ここで「名前を探す」などと言っているのはすべてコンパイル時の話です
コンパイラ内部の話です
実行時に関数を探すようなコードが生成されるわけではありません


ADL と関数テンプレート

標準に次のような記述があり、通常の関数テンプレート呼び出しには ADL は適用されるが、テンプレート引数を明示的に指定した場合は他の候補の名前が見えていないと ADL は適用されない(呼び出そうとしている Unqualified name がテンプレート関数だと認識されない)。と言っています

N1905
14 Templates
14.8 Function template specializations
14.8.1  Explicit template argument specification
8[Note:
For simple function names, argument dependent lookup (3.4.2) applies even when the function name is not visible within the scope of the call.
This is because the call still has the syntactic form of a function call (3.4.1).
But when a function template with explicit template arguments is used, the call does not have the correct syntactic form unless there is a function template with that name visible at the point of the call.
If no such name is visible, the call is not syntactically well-formed and argument-dependent lookup does not apply.
If some such name is visible, argument dependent lookup applies and additional function templates may be found in other namespaces.
[Example:
namespace A {
struct B { };
template<int X> void f(B);
}
namespace C {
template<class T> void f(T t);
}
void g(A::B b) {
	f<3>(b);	// ill-formed: not a function call
	A::f<3>(b);	// well-formed
	C::f<3>(b);	// ill-formed; argument dependent lookup applies only to unqualified names
	using C::f;
	f<3>(b);	// well-formed because C::f is visible; then A::f is found by argument dependent lookup
}
-end example]
-end note]

これは、テンプレートの非型引数が明示的に指定されているような記述と、数値比較をしようとしている記述の区別ができないので、そもそも関数呼び出しだと認識されません。テンプレート関数を呼びたいなら先にテンプレート関数の名前が見えていないとダメだよ。ということです

そう言われると、それはそうかなという決まりです

f<3>(b);	// テンプレート関数を呼び出したいつもり
( f < 3 ) > (b);	// でも数値比較と区別できないよね

今まではそうだったのですが、新しく導入した文法と折り合いが悪く C++20 ではこの制限が緩和されるようです
関数テンプレートに非型テンプレート引数を明示的に指定した場合でも正規の関数呼び出しであると認められ、ADL が働くようになるようです
(特に何もしなくても、呼び出そうとしている Unqualified name がテンプレート関数だと認識されるようになる)

N4713
6 Basic concepts
6.4 Name lookup
6.4.1 Unqualified name lookup
3 The lookup for an unqualified name used as the postfix-expression of a function call is described in 6.4.2.
[Note:
For purposes of determining (during parsing) whether an expression is a postfix-expression for a function call, the usual name lookup rules apply.
In some cases a name followed by < is treated as a template-name even though name lookup did not find a template-name (see 17.2).
For example,

int h;
void g();
namespace N {
struct A {};
template <class T> int f(T);
template <class T> int g(T);
template <class T> int h(T);
}
int x = f<N::A>(N::A());	// OK: lookup of f finds nothing, f treated as template name
int y = g<N::A>(N::A());	// OK: lookup of g finds a function, g treated as template name
int z = h<N::A>(N::A());	// error: h< does not begin a template-id

The rules in 6.4.2 have no effect on the syntactic interpretation of an expression.
For example,

typedef int f;
namespace N {
struct A {
	friend void f(A &);
	operator int();
	void g(A a) {
		int i = f(a);	// f is the typedef, not the friend function: equivalent to int(a)
	}
};
}

Because the expression is not a function call, the argument-dependent name lookup (6.4.2) does not apply and the friend function f is not found.
-end note]

これは今まで、テンプレート引数の明示的指定開始の < と比較演算子の < を区別できていなかったということなので、今回の修正は悪くは無いとは思います
ただ破壊的変更になるので、今まで問題なかったところで意図しない ADL が発動して困るようなことが起きるかもしれません
頭の隅に入れておくといいと思います

ADL と関数オブジェクト

標準に次のような記述があり、関数オブジェクトのような本当の関数(や関数テンプレート)でないものには ADL は適用されない。と言っています

N3000
3 Basic concepts
3.4 Name lookup
3.4.2 Argument-dependent name lookup
3 Let X be the lookup set produced by unqualified lookup (3.4.1) and let Y be the lookup set produced by argument dependent lookup (defined as follows).
If X contains
- a declaration of a class member, or
- a block-scope function declaration that is not a using-declaration, or
- a declaration that is neither a function or a function template
then Y is empty.
Otherwise Y is the set of declarations found in the namespaces associated with the argument types as described below.
The set of declarations found by the lookup of the name is the union of X and Y.
[Note:
the namespaces and classes associated with the argument types can include namespaces and classes already considered by the ordinary unqualified lookup.
-end note]
[Example:
namespace NS {
class T { };
	void	f(T);
	void	g(T, int);
}
NS::T	parm;
void	g(NS::T, float);
int	main() {
	f(parm);	// OK: calls NS::f
	extern void g(NS::T, float);
	g(parm, 1);	// OK: calls g(NS::T, float)
}
-end example]

一見すると同じ関数呼び出しなのに関数オブジェクトのときだけ ADL が働かないのはおかしいじゃないかと感じるかもしれませんが、ADL というのは関数を探す仕組みだということを思い出してください

関数オブジェクト呼び出しでは呼び出すべきものはもう決まっています。探すべき関数なんてものは存在しないということに気付けば何もおかしくはないということがわかると思います


今まで特に注目されていなかったこの仕組みですが、C++20 では、この関数呼び出しのように見えるのに ADL が働かないという性質がカスタマイゼーションポイントオブジェクト(CPO)として利用されるようです


ADL とカスタマイゼーションポイント

カスタマイゼーションポイントオブジェクト(CPO)の前に、カスタマイゼーションポイントという考え方が出てきます


C++11 で std::begin, std::end 等の関数が標準ライブラリに追加されました

N3000
24 Iterators library
24.3 Header <iterator> synopsis
namespace std {
.
.
.
// 24.6.5, range access:
template <class C> auto begin(C& c) -> decltype(c.begin());
template <class C> auto begin(const C& c) -> decltype(c.begin());
template <class C> auto end(C& c) -> decltype(c.end());
template <class C> auto end(const C& c) -> decltype(c.end());
template <class T, size_t N> T* begin(T (&array)[N]);
template <class T, size_t N> T* end(T (&array)[N]);
}

この追加は単純に便利な関数を追加しただけというくらいの認識だったようですが、これが後に現れるカスタマイゼーションポイントオブジェクト(CPO)へのきっかけとなりました


標準テンプレートライブラリの実装を考えてみましょう
標準テンプレートライブラリの実装は当然 std 名前空間内にあるので、これらの関数を呼び出す場合にいちいち std:: の名前空間指定を付ける必要はなく、auto p = begin( c ); 等と書くことになります
すると、この関数呼び出しは Unqualified name lookup になり、ADL が働くことになります


今まで auto p = c.begin(); 等と書いていたところを auto p = begin( c ); と書くようにした。ただそれだけのことなのですが、意図せず ADL が導入されるようになってしまいました

当初この事実はあまり重要視されていなかったようなのですが、近年になってこの ADL をカスタマイゼーションポイントとして使えるのではないかということが認識されるようになってきました


最近の標準では、次のような記述が追加されました

N4741
20 Library introduction
20.5 Library-wide requirements
20.5.4 Constraints on programs
20.5.4.2  Namespace use
20.5.4.2.1  Namespace
7 Other than in namespace std or in a namespace within namespace std, a program may provide an overload for any library function template designated as a customization point, provided that (a) the overload's declaration depends on at least one user-defined type and (b) the overload meets the standard library requirements for the customization point.(179
[Note:
This permits a (qualified or unqualified) call to the customization point to invoke the most appropriate overload for the given arguments.
-end note]

179)
Any library customization point must be prepared to work adequately with any user-defined overload that meets the minimum requirements of this document.
Therefore an implementation may elect, under the as-if rule (6.8.1), to provide any customization point in the form of an instantiated function object (23.14) even though the customization point's specification is in the form of a function template.
The template parameters of each such function object and the function parameters and return type of the object's operator() must match those of the corresponding customization point's specification.

関数オブジェクトの形でと言っているのが、カスタマイゼーションポイントオブジェクト(CPO)のことです


ADL とカスタマイゼーションポイントオブジェクト(CPO)

このカスタマイゼーションポイントオブジェクト(CPO)についても当初はあまり重要視されていなかったようなのですが、関数オブジェクトの呼び出し時に ADL が働かないという性質が実は重要なのではないかということが認識されるようになります


最近の標準には、次のような記述が追加されました

N4762
15 Library introduction
15.4 Method of description (Informative)
15.4.2 Other conventions
15.4.2.1 Type descriptions
15.4.2.1.6  Customization Point Object types
1 A customization point object is a function object (19.14) with a literal class type that interacts with program-defined types while enforcing semantic requirements on that interaction.
2 The type of a customization point object shall satisfy Semiregular(17.6).
3 All instances of a specific customization point object type shall be equal (17.2).
4 The type T of a customization point object shall satisfy Invocable<const T&, Args...> (17.7.2) when the types in Args... meet the requirements specified in that customization point object's definition.
When the types of Args... do not meet the customization point object's requirements, T shall not have a function call operator that participates in overload resolution.
5 Each customization point object type constrains its return type to satisfy a particular concept.
6 [Note:
Many of the customization point objects in the library evaluate function call expressions with an unqualified name which results in a call to a program-defined function found by argument dependent name lookup (6.4.2).
To preclude such an expression resulting in a call to unconstrained functions with the same name in namespace std, customization point objects specify that lookup for these expressions is performed in a context that includes deleted overloads matching the signatures of overloads defined in namespace std.
When the deleted overloads are viable, program-defined overloads need be more specialized (12.6.6.2) or more constrained (12.4.4) to be used by a customization point object.
-end note]

カスタマイゼーションポイントオブジェクト(CPO)というのは、ADL を制御しようとする試みです
関数呼び出し時に適用される ADL はそのままでは制御不能で、もし意図しない適用が起きていたとしてもそれを検出できるのは実行時でした
それでは困りますね
ここで関数オブジェクトが注目されます


関数オブジェクトは本当の関数ではないので、ADL は適用されません
必ず意図したものが呼ばれるようにコンパイルされますから、そこに何かのチェックを咬ませることが簡単にできます
C++20 にはコンセプトが導入されますから、このコンセプトで必要なチェックをした後に、ADL が有効な文脈に実引数を渡してあげると ADL を制御下に置くことができるようになります


つまり、カスタマイゼーションポイントオブジェクト(CPO)というのは、ADL を制御下に置くための仕組みです

ADL が制御不能であるというところは変わりませんが、その前にコンパイル時のチェックができるということで幾分安心できるようになりそうです


niebloid

現在一般的にカスタマイゼーションポイントオブジェクト(CPO)と呼ばれているものは当初、その発案者である Eric Niebler 氏によって niebloid と呼ばれていました
知っている人は今も niebloid と呼ぶこともありますが、niebloid というのはカスタマイゼーションポイントオブジェクト(CPO)のことです

std::tag_invoke

C++20 の次の C++23 で導入を目指している std::tag_invoke などもその一つですが、カスタマイゼーションポイントオブジェクト(CPO)に関連したところは標準でもまだ発展途中なので、そんなに急いで導入する必要はないと思います
将来のための基礎知識として今のところはなんとなく理解していればそれでいいと思います