"Poison-pill" overload for CPO

C++20標準ライブラリで導入された Customization Point Object (CPO)定義で必要となる Poison-pill*1 オーバーロードについてメモ。std::ranges::swapstd::ranges::begin/endなどのCPO定義で利用される。


std::ranges名前空間でのCPO定義位置からは、親名前空間stdで定義されるカスタマイズポイント同名の制約のない関数テンプレート(std::swapstd::begin/end)が "見えて” しまうため、同関数テンプレートシグネチャをdelete宣言してオーバーロード候補から除外する(★印)。

// std::ranges::swap CPO実装イメージ(超簡略化版)
namespace std::ranges {
  namespace swap_impl {
    template<class T> void swap(T&, T&) = delete;  // ★

    struct swap_fn {
      template<class T1, class T2>
        requires /* C++20 18.4.9/p2/b1 */
      constexpr void operator()(T1& e1, T2& e2) const {
        // 非修飾名・ADL経由でカスタマイズポイント(swap)を呼び出す
        swap(e1, e2);
      // ...

  // std::ranges::swap CPO定義
  inline namespace swap_cpo {
    inline constexpr swap_impl::swap_fn swap{};
    // Hidden friendとの名前衝突回避のためインライン名前空間が必要
    // 詳細説明は提案文書 P1895R0 を参照のこと

C++20 Rangesライブラリの前身、Ranges TS検討当時の提案文書 P0370R3 Ranges TS Design Updates Omnibus より一部引用。

unqualified name lookup for the name swap could find the unconstrained swap in namespace std either directly - it’s only a couple of hops up the namespace hierarchy - or via ADL if std is an associated namespace of T or U. If std::swap is unconstrained, the concept is "satisfied" for all types, and effectively useless. The Ranges TS deals with this problem by requiring changes to std::swap, a practice which has historically been forbidden for TSs. Applying similar constraints to all of the customization points defined in the TS by modifying the definitions in namespace std is an unsatisfactory solution, if not an altogether untenable.

We propose a combination of the approach used in N4381 with a "poison pill" technique to correct the lookup problem. Namely, we specify that unqualified lookup intended to find user-defined overloads via ADL must be performed in a context that includes a deleted overload matching the signature of the implementation in namespace std. E.g., for the customization point begin, the unqualified lookup for begin(E) (for some arbitrary expression E) is performed in a context that includes the declaration void begin(const auto&) = delete;. This "poison pill" has two distinct effects on overload resolution. First, the poison pill hides the declaration in namespace std from normal unqualified lookup, simply by having the same name. Second, for actual argument expressions for which the overload in namespace std is viable and found by ADL, the poison pill will also be viable causing overload resolution to fail due to ambiguity. The net effect is to preclude the overload in namespace std from being chosen by overload resolution, or indeed any overload found by ADL that is not more specialized or more constrained than the poison pill.


[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.5.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 ( or more constrained (13.5.4) to be used by a customization point object. -- end note]