オーバーロード解決の制御 #3:タグのディスパッチ

オーバーロードの解決は C++ で最も複雑なことの 1 つですが、ほとんどの場合、それについて考える必要はありません。あなたのコントロール。

3 番目の投稿では、(テンプレート化された) 関数の複数の実装から選択するためのタグ ディスパッチの力を示しています。これにより、特殊なプロパティを持つ型の強力な最適化が可能になります。

モチベーション

たとえば、関数 05 があるとします。 19 型の配列の初期化されていないメモリまでの範囲を取る その中にデフォルトで構築されたオブジェクトを作成します。この関数は 22 の呼び出し後に使用できます たとえば、その中に実際の要素を作成します。

これを簡単に実装すると、次のようになります。

#include <new>

template <typename T>
void construct(T *begin, T *end)
{
 for (auto cur = begin; cur != end; ++cur)
 ::new(static_cast<void*>(cur)) T(); 
}

ただし、この単純な実装には欠陥があります。例外セーフではありません。 th コンストラクター呼び出しは例外をスローします。以前のすべてのオブジェクトは既に作成されており、破棄する必要がありますが、例外が伝播され、関数は部分的に構築された範囲で戻ります。呼び出し元には、構築された要素を破棄するために必要な情報さえありません。 、作成された数がわからないためです!

41 を入れて修正しましょう -54 ループの周り:

#include <new>

template <typename T>
void construct(T *begin, T *end)
{
 auto cur = begin;
 try
 {
 for (; cur != end; ++cur)
 ::new(static_cast<void*>(cur)) T(); 
 }
 catch (...)
 {
 for (auto new_cur = begin; new_cur != cur; ++new_cur)
 new_cur->~T();
 throw; 
 }
}

62 の場合 th コンストラクターが例外をスローすると、作成されたすべての要素が破棄されます。関数は、すべての要素が作成されたか、または何も返されないかのいずれかでのみ返されます。

しかし 72 -87 バージョンは、ないものよりも高価です。また、デフォルトのコンストラクタが 95 の場合は不要です。 は例外をスローしません。また、ライブラリの作成者として、そのような時期尚早な最適化を実行して、最大のパフォーマンスを引き出すことができるので、実行しましょう.

最も単純なタグのディスパッチ - 105 /113

タグ ディスパッチは、型のプロパティに基づいて (テンプレート化された) 関数の特定の実装を選択するための非常に強力な手法です。追加の引数 (関数呼び出しに渡されるタグ) を使用します。オーバーロードが選択されます。

127 で 上記の例では、2 種類の実装があります。1 つ目は型のデフォルト コンストラクターが例外をスローしない場合に使用でき、2 つ目は型が例外をスローしない場合に使用できます。

最も基本的なタグの種類は 136 です と 141 ヘッダー 155 で定義 、ここのように実装が 2 つしかない場合。

それでは、それらを入れてみましょう:

#include <new>
#include <type_traits>

template <typename T>
void construct(std::true_type, T *begin, T *end)
{
 for (auto cur = begin; cur != end; ++cur)
 ::new(static_cast<void*>(cur)) T(); 
}

template <typename T>
void construct(std::false_type, T *begin, T *end)
{
 auto cur = begin;
 try
 {
 for (; cur != end; ++cur)
 ::new(static_cast<void*>(cur)) T(); 
 }
 catch (...)
 {
 for (auto new_cur = begin; new_cur != cur; ++new_cur)
 new_cur->~T();
 throw; 
 }
}

それのポイントは何ですか、あなたは尋ねます。さて、タグに基づいて実装を選択できるようになりました。スローしないコンストラクタがある場合は、160 を渡します。 最初の引数として、それ以外の場合は 171 .

しかし、それはあまり便利ではありません。どの型のデフォルト コンストラクターがスローされず、変更された場合はリファクタリングされないかを覚えておく必要があります。そして、180かどうか知っていますか? のデフォルト コンストラクターが例外をスローしますか?

型特性を入力してください:ヘッダー 192 型情報に関する便利なクエリを多数提供します。たとえば、203 メンバー定数 216 を提供します 型がデフォルトで構築可能でない場合 (duh)、それ以外の場合は定数 220 .そしてメンバー定数は 230 から継承して挿入されているので /242 、これはオーバーロードに正確にマップされます!

これにより、254 を呼び出すことができます

construct(std::is_nothrow_default_constructible<std::string>{}, beg, end);

ええ、まだ醜いですが、少なくとも保守可能です。

そのため、タグ ディスパッチされたオーバーロードは、タグ引数なしで親関数によって呼び出されることが多く、適切なタグ タイプを挿入した後に転送するだけです:

template <typename T>
void construct(T *begin, T *end)
{
 construct(std::is_nothrow_default_constructible<T>{}, begin, end);
}

これにより、タグ ディスパッチの使用がユーザーに対して完全に透過的になります。関数に渡す必要があるのは 2 つのポインターだけで、残りは魔法によって行われます。

タグの拡張:複数のタグ引数

しかし、議論のために、私がまだ 263 に満足していないとしましょう 汎用コードで使用すると、必要以上の作業を行うことがあります。たとえば、278 を構築します。 何もしないので、呼び出す必要のあるコンストラクターはありません!

281 の場合 その他のすべての型は、その点については自明なデフォルト コンストラクターを持ち、295 の本体です。 完全に空にすることができます。

これを例外のタグ ディスパッチと組み合わせると、次のようになります。

nothrow ctor 自明な俳優 実装
309 313 ノーオペレーション
322 330 346 を使用しない最初の実装 -357
367 377 該当なし (組み合わせ不可)
388 395 404 を使用した 2 番目の実装 -415

実装のオーバーロードごとに 2 つのタグ引数があり、組み合わせをチェックします:

template <typename T>
void construct(std::true_type, std::true_type, T *, T *) {} // no-op overload

template <typename T>
void construct(std::true_type, std::false_type, T *begin, T *end)
{
 simple loop 
}

template <typename T>
void construct(std::false_type, std::false_type, T *begin, T *end)
{
 try catch loop
}

同様に、親オーバーロードは 2 つの引数を渡す必要があります:

template <typename T>
void construct(T *begin, T *end)
{
 construct(std::is_nothrow_default_constructible<T>{},
 std::is_trivially_default_constructible<T>{},
 begin, end);
}

タグの拡張:N-ary 特性

しかし、上記のアプローチはあまりエレガントではなく、簡単に手に負えなくなります。より良いアプローチは 421 複数の 434 ではなく、異なるタグ タイプ /443 引数。

3 つのケースを表すために、次のように 3 つのタイプを定義します。

struct trivial_default_ctor {};
struct nothrow_default_ctor {};
struct default_ctor {};

これらは、457 を区別するために使用する 3 つのタグ タイプです。 次に、タイプをこれらのタグにマップする小さな特性を作成します:

template <typename T>
struct default_ctor_information // I hate to come up with those names...
{
private:
 using is_nothrow = std::is_nothrow_default_constructible<T>;
 using is_trivial = std::is_trivially_default_constructible<T>;
 
 using nothrow_conditional = typename std::conditional<is_nothrow::value, nothrow_default_ctor, default_ctor>::type;
 
public:
 using type = typename std::conditional<is_trivial::value, trivial_default_ctor, nothrow_conditional>::type;
};

この特性は、同じ型特性と 467 を使用するだけです。 条件に基づいてタイプを選択します。これは、親 478 で使用できるようになりました オーバーロード:

template <typename T>
void construct(T *begin, T *end)
{
 construct(typename default_ctor_information<T>::type,
 begin, end);
}

この手法のもう 1 つの利点は、実装をより明確にするタグに独自の名前を選択できることです。

タグを優先的にディスパッチ

上記の 3 つのタグ タイプを見ると、それらの間に関係があることがわかります。A 486 491 を意味します これは 504 を意味します .このような関係は C++ では継承によって表現されるため、ホース タグ タイプは互いに継承できます。

struct default_ctor {};
struct nothrow_default_ctor : default_ctor {};
struct trivial_default_ctor : nothrow_default_ctor {};

これには興味深い結果があります:512 型の引数 529 に暗黙的に変換できるようになりました および 536 、オーバーロードの解決に影響します:オーバーロードには優先度チェーンがあります。暗黙的な変換シーケンスのランキングで指定されているように、コンパイラは最初に型自体を照合し、次にその直接の基底クラスを照合し、次に基底クラスの基底クラスを照合します。

これにより、たとえば、自明な型の no-op オーバーロードを削除でき、すべてが引き続き機能します。オーバーロードの解決により、直接の基本クラスでオーバーロードが選択されます - 546 .例外をスローしない特殊なケースについても同様です。

結論

タグ ディスパッチは、型の特定のプロパティに基づいて異なる実装を選択できる非常に強力な手法です。ユース ケースの 1 つは、特定の型のセットがジェネリック型よりも効率的に処理できる場合の最適化です。

タグ ディスパッチを使用するには、一連のタグ タイプを作成します (または 550 などの定義済みのものを使用します) /566 ) 多くの場合、概念の改良階層に似たクラス階層を通じて関連付けられます。各実装は、最初の引数としてタグ型の 1 つを取ります。タグ引数のない親オーバーロードは、適切なタグ型を選択します。たとえば、型をマップする特性クラスを通じてです。タグに追加し、それを実装のオーバーロードに渡します。オーバーロード解決の魔法により、適切な (または階層の場合は最適な) タグを持つ実装が選択されます。

このシリーズの次の投稿では、さまざまなユース ケースでタグ ディスパッチに代わる方法、SFINAE について説明します。