C++11 以降でのカスタマイズ ポイントの設計

(免責事項:ここでは難解な言葉遣いです。すべての希望を捨ててください。)

Generic-code-with-a-capitol-'G' を読んだり書いたりする場合は、次のようなコードを書いたり見たりしたことがあるかもしれません:

using std::swap;
swap( a, b );

最初の行は 03 をもたらします 考慮され、2 番目は 13 への非修飾呼び出しを行います 関数。私はこれを「Std Swap Two-Step」と呼んでいます。

なぜツーステップを行うのですか?これは、C++ がテンプレート内の関数呼び出しを解決する明らかに不安定な方法に関係しています。 2 段階の名前検索については詳しく説明しません (どういたしまして) が、汎用性のためにそうしていると言えれば十分です。 38 で定義されている可能性のあるオーバーロードを見つけるため と 43 の関連する名前空間 (それ自体が豊富なトピック) であり、57 を実行します。 そのため、そのようなオーバーロードがない可能性が低い場合は、60 で定義されているデフォルト バージョンを見つけます。 名前空間。

73 と呼びます およびそのような機能 カスタマイズ ポイント — エンドユーザーが型の動作をカスタマイズするために特殊化できる汎用コードで使用されるフック。

その他の標準的なカスタマイズ ポイントはありますか?あなたは賭けます。範囲ベースの 89 を使用する場合 ループ、コンパイラは修飾されていない 92 への呼び出しを挿入します そして 100 範囲の境界を取得します。だから 110125 カスタマイズのポイントです。 133 の仕様の読み方次第 、 143 カスタマイズのポイントにもなります。 (そうあるべきか、そうあるべきだと思います。)そして、他のカスタマイズポイントは沖にあるかもしれません。提案 N4155、非メンバー 151 を提案 範囲のサイズを取得するために、私自身の N4128 は 165 を提案します カスタマイズのポイントとしても。

ツーステップのトラブル

修飾するコードを見たことがありますか 174 への呼び出し 189 のようなテンプレートで ?おめでとうございます。おそらくバグを発見したことでしょう。 193 の型の場合 と 208 オーバーロード 219 独自の名前空間で、228 への修飾呼び出し 見つかりません。犯しやすい間違いです。

Two-Step の問題は、ユーザーが more と入力する必要があることです。 正しいことをすること。不合格。最もひどいのは、ユーザーが 2 段階のパターンをやみくもに記憶して吐き出すか、さらに悪いことに、テンプレートでの 2 段階の名前検索を理解することをユーザーに要求することです。 <戦慄>

ツーステップで

C++ でのカスタマイズ ポイントの設計には、より優れたソリューションが必要です。私自身の範囲ライブラリーで、この問題についてよく考えてみたところ、答えがあると思います。以下は、標準ライブラリの将来のバージョンで 230 を定義する方法です。 、例をランダムに選択します。休憩後に説明します:

namespace std
{
  namespace __detail
  {
    // define begin for arrays
    template<class T, size_t N>
    constexpr T* begin(T (&a)[N]) noexcept
    {
      return a;
    }

    // Define begin for containers
    // (trailing return type needed for SFINAE)
    template<class _RangeLike>
    constexpr auto begin(_RangeLike && rng) ->
      decltype(forward<_RangeLike>(rng).begin())
    {
      return forward<_RangeLike>(rng).begin();
    }

    struct __begin_fn
    {
      template<class R>
      constexpr auto operator()(R && rng) const ->
        decltype(begin(forward<R>(rng)))
      {
        return begin(forward<R>(rng));
      }
    };
  }

  // To avoid ODR violations:
  template<class T>
  struct __static_const
  {
    static constexpr T value{};
  };

  template<class T>
  constexpr T __static_const<T>::value;

  // std::begin is a global function object!
  namespace
  {
    constexpr auto const & begin =
        __static_const<__detail::__begin_fn>::value;
  }
}

これを分解しましょう。まず、いくつかの 241 を定義します 254 のフリー関数 名前空間。これらのオーバーロードは、配列型と範囲のようなものを 268 で処理します メンバー関数。 (標準のコンテナを考えてください。)

次に、278 を定義します 280 でオーバーロードされた関数呼び出し演算子を持つクラス 292 への非修飾呼び出しの結果を返す名前空間 .ソースコードのこの時点で、名前 301 関数オーバーロード セットを参照します。

最後に、317 を定義します オブジェクト タイプ 320 の 回りくどい方法で、その詳細はあまり関連性がありません。重要なのは 333 です 関数オブジェクトです。

範囲のような型の実装者は、これまでと同じ方法でこのカスタマイズ ポイントをフックできます:347 を定義することによって それらの型に関連付けられた名前空間の free 関数。以下を参照してください:

namespace NS {
  struct S {};
  int * begin( S & s );
}

int main() {
  NS::S s;
  int *p = std::begin(s); // calls NS::begin(s)
}

関数オブジェクトとカスタマイズ ポイント

引数に依存するルックアップとカスタマイズ ポイントは、理想的な組み合わせです。ただし、引数に依存するルックアップは、フリー関数に対してのみ行われます 、そして私の 351 関数 オブジェクト です .関数オブジェクトでは、引数依存のルックアップは行われません。何が起きているの?

簡単に言えば、360 関数オブジェクトはツーステップを実行しているので、あなたがする必要はありません。 378 の場合 このように定義されていれば、修飾することができます 384 への呼び出し そして正しいことが起こるでしょう。 395 をもたらすツーステップを実行することもできます 406 でスコープに入る 宣言し、それを非修飾で呼び出すと、同じ動作が得られます .どちらにしても 415 があれば 引数に関連付けられた名前空間で定義された free 関数は、使用されます。

微妙ではありますが重要な点は、Two-Step を実行した場合でも、通話は 426 を介してルーティングされるということです。 関数オブジェクト。以下のコードでそれを意味します:

using std::begin;
begin( v );

…if 433 関数ではなくオブジェクトだった場合、修飾されていない関数呼び出しのように見えるものはそうではありません。 442 への呼び出しです のオーバーロードされた関数呼び出し演算子。これは、Gang of Four の Template メソッド パターンに相当する一般的なものと考えてください。

この場合、「アルゴリズム」は 453 です。 、ユーザーが再定義できる特定のステップは 469 です .ポイントは何ですか、あなたは尋ねますか? 472 で追加のパラメーター チェックを行うことができます .読み進めてください。

カスタマイズのポイントとコンセプト ライト

カスタマイズポイントはある意味怖いです。今日の言語では、482 というフリー関数を定義すると 、標準ライブラリが 495 と期待することを行うほうがよい する。そうしないと、標準のアルゴリズムですべての地獄が解き放たれます。同様に、 505 を定義すると、自分自身を撃つことができます または 516 イテレータを返さない free 関数。そのため、標準ライブラリはこれらの名前をグローバルに主張しています。 .そのため、標準化委員会がカスタマイズ ポイントを非常に懸念しています。追加すればするほど、グローバルに予約する名前が増え、ユーザーにとって潜在的な問題が大きくなります。

コンセプトライトに入ります。 Concepts Lite を使用すると、特定の概念をモデル化する型でのみ機能するようにカスタマイズ ポイントを制限できます。たとえば、524 を呼び出すとエラーになるはずです 範囲のように見えないものについて、そう思いませんか? Concepts Lite とグローバル関数オブジェクトを使用すると、それを実現できます。 539 を定義できます このように:

// A _RangeLike is something we can call begin(r)
// and end(r) on:
concept _RangeLike<class T> =
  requires(T t) {
    typename IteratorType<T>;
    { begin(t) } -> IteratorType<T>;
    { end(t) } -> IteratorType<T>;
    requires Iterator<IteratorType<T>>;
  };

  struct __begin_fn
  {
    // LOOK! R must be _RangeLike!
    template< _RangeLike R >
    constexpr auto operator()(R && rng) const ->
      decltype(begin(forward<R>(rng)))
    {
      return begin(forward<R>(rng));
    }
  };

まず、542 を呼び出すことができるものとして _RangeLike の概念を定義します。 と 551 、両方が同じ型の反復子を返すようにします。 (または、N4128 に同意する場合は、比較可能な異なる型。) 次に、_RangeLike の概念を使用して 567 を制約します。 および拡張子 574 で .現在 584 598 のような一般的な識別子を主張する方が安全です。 .

604 の場合 関数 オブジェクト です フリー関数とは対照的に、この概念チェックを回避するのは簡単ではありません。 Two-Step を実行するコードは、関係のない 615 を誤ってハイジャックすることはありません ランダムな名前空間で機能します。常に 627 に解決されます 、無効なコードを丁重に拒否します。

Concepts Lite が恩恵を受けるのを待つ必要もありません。 C++11 での Concepts Lite のエミュレートに関する私の投稿を参照してください。

まとめ

これはどういう意味ですか?簡単に:

  • ユーザーは 639 を呼び出すだけです そしてそれは彼らのために ADL を行います。
  • 643 次の場合を除き、コンパイルされません:
    • イテレータを返し、
    • 656 また コンパイルして、同じ型の反復子を返します。
  • 664 を実行するコード ランダムな671にディスパッチするつもりはありません 引数が 686 の制約を満たさない限り機能します .

より一般的には、安全で便利なカスタマイズ ポイントを作成するために使用できるデザイン パターンがあります。カスタマイズ ポイントを含む汎用ライブラリを作成している場合は、このパターンを使用することをお勧めします。

補遺:グローバル関数オブジェクトへの頌歌

691 を作成すると、さらにメリットがあります。 グローバル関数オブジェクト:

  • 700 を渡すことができます 高階関数への引数として。

これは、一般的に無料の関数よりも関数オブジェクトの利点であり、私が最近一般的に無料の関数よりもグローバル関数オブジェクトを好む理由です (カスタマイズ ポイントを定義する場合を除く)。グローバル関数オブジェクトを定義するのは手間がかかりますが、引数依存のルックアップをオフにするという素晴らしい効果があります。これは、実際には演算子のオーバーロードとカスタマイズ ポイントに対してのみ意味があります。一階関数則。 ADL はひどいものです (いくつかの素晴らしい場所を除いて)。

更新

質問をいただいたので、ジェネリック ラムダについて簡単に説明します。 C++14 では、汎用ラムダを使用して非常に簡潔に多相関数オブジェクトを定義できます。以下のように、ラムダを使用してグローバル関数オブジェクトを定義し、入力を節約できます:

// Better?
constexpr auto begin = [](auto && rng) {
  using __detail::begin;
  return begin(forward<decltype(rng)>(rng));
};

悲しいことに、多くの理由から答えはノーです:

<オール>
  • ラムダには 716 がありません コンストラクタ。
  • ラムダの ODR 問題を解決する方法がわかりません。 722 の場合 このように定義された場合、各翻訳単位は異なる 735 を参照します 異なるアドレスのオブジェクト。理論的には、問題が発生する可能性があります。
  • ジェネリック ラムダを制約する方法がわかりません。
  • 戻り型の自動推定では、743 の無効な呼び出し SFINAE で取り除かれるのではなく、ハード エラーを引き起こします。 754 にとっては大きな問題ではないかもしれません 、しかし、それは間違いなく そうです 769 の大きな問題 . 774 ADL must によって検出されたオーバーロード SFINAE (または概念チェック) を使用します。そうしないと、786 を呼び出そうとすることになります。 795 を持たないオブジェクト メンバー関数。
  • 要するに、C++14 でも、私が示した醜いハッカーが必要だと思います。多分 C++17 が安心をもたらすでしょう。

    "\e"

    "\e"