C++標準の最後のドラフトでは、範囲ライブラリで広く使用されている、いわゆる「カスタマイズポイントオブジェクト」( [customization.point.object] )を紹介しています。
ADLが標準ライブラリで見つけたbegin
、swap
、data
などのカスタムバージョンを作成する方法を提供していることを理解しているようです。あれは正しいですか?
これは、ユーザーがオーバーロードを定義する以前のプラクティスとどのように異なりますか。 begin
自分の名前空間にある彼女のタイプは?特に、なぜobjectsなのか?
カスタマイズポイントオブジェクトとは何ですか?
これらは、名前空間std
の関数オブジェクトインスタンスであり、2つの目的を満たします。first無条件でトリガーの(概念)型の要件を引数にトリガーしますその後、ネームスペースstd
の適切な関数に、またはADL経由でディスパッチします。
特に、なぜobjectsなのか?
これは、ADLを介してユーザー提供の関数を直接取り込む2番目のルックアップフェーズを回避するために必要です(これは延期仕様によるものです)。詳細については、以下を参照してください。
...そしてそれらをどのように使用しますか?
アプリケーションを開発する場合:主に開発しません。これは標準のライブラリ機能であり、将来のカスタマイズポイントにコンセプトチェックを追加します。テンプレートのインスタンス化を台無しにすると、明確なエラーメッセージが表示されます。ただし、そのようなカスタマイズポイントへの修飾された呼び出しがあれば、それを直接使用できます。以下は、デザインに忠実な架空の_std::customization_point
_オブジェクトの例です。
_namespace a {
struct A {};
// Knows what to do with the argument, but doesn't check type requirements:
void customization_point(const A&);
}
// Does concept checking, then calls a::customization_point via ADL:
std::customization_point(a::A{});
_
これは現在、たとえば、 _std::swap
_、_std::begin
_など。
標準のこのセクションの背後にある提案を要約してみましょう。標準ライブラリで使用される「クラシック」カスタマイズポイントには2つの問題があります。
彼らは間違いを犯しやすい。例として、汎用コードでオブジェクトを交換すると、次のようになるはずです。
_template<class T> void f(T& t1, T& t2)
{
using std::swap;
swap(t1, t2);
}
_
しかし、代わりにstd::swap(t1, t2)
への修飾された呼び出しを行うのは簡単すぎます。ユーザーが提供したswap
が呼び出されることは決してありません( N4381 、モチベーションとスコープを参照)
さらに厳しく、そのようなユーザー提供の関数に渡される型の(概念化された)制約を集中化する方法はありません(これは、このトピックがC++ 20で重要になった理由でもあります)。再び N4381 から:
_
std::begin
_の将来のバージョンでは、その引数モデルがRangeコンセプトを必要とするとします。このような制約を追加しても、_std::begin
_を慣用的に使用するコードには影響しません。
_using std::begin;
_begin(a);
beginの呼び出しがユーザー定義のオーバーロードにディスパッチする場合、_std::begin
_の制約はバイパスされています。
提案で説明されているソリューションは、次のような架空の実装_std::begin
_のようなアプローチによって両方の問題を軽減します。
_namespace std {
namespace __detail {
/* Classical definitions of function templates "begin" for
raw arrays and ranges... */
struct __begin_fn {
/* Call operator template that performs concept checking and
* invokes begin(arg). This is the heart of the technique.
* Everyting from above is already in the __detail scope, but
* ADL is triggered, too. */
};
}
/* Thanks to @cpplearner for pointing out that the global
function object will be an inline variable: */
inline constexpr __detail::__begin_fn begin{};
}
_
最初に、たとえばstd::begin(someObject)
は常に_std::__detail::__begin_fn
_経由で迂回しますが、これは望ましいことです。不適格な呼び出しで何が起こるかについて、私は再び元の論文を参照します。
_
std::begin
_をスコープに組み込んだ後でbeginが修飾なしで呼び出された場合、状況は異なります。ルックアップの最初のフェーズでは、名前beginはグローバルオブジェクト_std::begin
_に解決されます。ルックアップで関数ではなくオブジェクトが見つかったため、ルックアップの第2フェーズは実行されません。つまり、_std::begin
_がオブジェクトの場合、using std::begin; begin(a);
はstd::begin(a);
と同等です。これは、すでに見てきたように、ユーザーの代わりに。
このように、std
名前空間の関数オブジェクト内で概念チェックを実行できます。beforeユーザー提供の関数へのADL呼び出しが実行されます。これを回避する方法はありません。
「カスタマイズポイントオブジェクト」は、少し間違っています。多く(おそらく過半数)は、実際にはカスタマイズポイントではありません。
ranges::begin
、ranges::end
、ranges::swap
などは「真の」CPOです。それらのいずれかを呼び出すと、呼び出すカスタマイズされたbegin
またはend
またはswap
があるかどうか、またはデフォルトの実装が使用するか、または代わりに(SFINAEに適した方法で)呼び出しの形式を変更する必要がある場合。多くのライブラリの概念はCPO呼び出しが有効であるという観点から定義されているため(Range
やSwappable
など)、正しく制約された汎用コードはそのようなCPOを使用する必要があります。もちろん、具体的なタイプと、イテレータをそこから取り出す別の方法を知っている場合は、遠慮なくお問い合わせください。
ranges::cbegin
のようなものは、「CP」部分のないCPOです。彼らは常にデフォルトのことをするので、それはカスタマイズのポイントではありません。同様に、範囲アダプターオブジェクトはCPOですが、それらについてカスタマイズ可能なものはありません。それらをCPOとして分類することは、一貫性の問題(cbegin
の場合)または仕様の利便性(アダプター)の問題です。
最後に、ranges::all_of
のようなものは、準CPOまたはniebloidsです。それらは、代わりに関数オブジェクトとして実装できるように、特別な魔法のADLブロッキングプロパティとイタチ表現を備えた関数テンプレートとして指定されます。これは主に、std::ranges
の制約付きアルゴリズムが修飾なしで呼び出されたときに、ADLが名前空間std
の制約なしのオーバーロードを取得しないようにするためです。 std::ranges
アルゴリズムはイテレータとセンチネルのペアを受け入れるため、通常はstd
の対応するものよりも専門性が低く、結果として過負荷の解決策が失われます。