特性クラスは非常に強力です。これらにより、情報と機能を非侵入的な方法で任意のクラスに関連付けることができます。これにより、すべてのアクセスが特性を介して行われ、適切な
多くの場合、デフォルトの特殊化はメンバー関数に転送されるだけです。ただし、一部の機能では、タイプが必ずしもそれを提供する必要はなく、オプションです。次に、特性は、フォールバックとして使用されるデフォルトの実装を定義します。この投稿では、そのような特性クラスを実装する方法.
{% assign series-menu-name ="制御オーバーロード解決:" %}{% assign series-menu-series ="制御オーバーロード解決" %}{% include post_series_menu.html %}
モチベーション
C++11 09
次の機能を提供するだけで済みます:
#include <cstddef>
template <class Tp>
struct SimpleAllocator {
typedef Tp value_type;
SimpleAllocator(/*ctor args*/);
template <class T> SimpleAllocator(const SimpleAllocator<T>& other);
Tp* allocate(std::size_t n);
void deallocate(Tp* p, std::size_t n);
};
template <class T, class U>
bool operator==(const SimpleAllocator<T>&, const SimpleAllocator<U>&);
template <class T, class U>
bool operator!=(const SimpleAllocator<T>&, const SimpleAllocator<U>&);
(cppreference からコピー&ペースト)
ただし、オプションで 14
たとえば、ポインターの型やオブジェクトの構築を制御できます。cppreference の表を見ると、多くのメンバーが「オプション」とマークされています。これはどのように達成されますか?
答えは特性クラス 26
です .
異なるインターフェイスを持つ任意のユーザー定義型に特化する機能を提供するだけでなく、デフォルトのフォールバックも提供します。たとえば、クラスがメンバー typedef 39
を提供しない場合 、デフォルトの 45
を提供します .これがどのように行われるかは、このブログ投稿のトピックです。
課題
しかし 59
退屈ですが、実装はとても簡単です!
代わりに 66
を見てみましょう from foonathan/memory.
ライブラリには、新しいアロケータの概念 77
があります。 .traits クラスも 82
を受け入れる必要があります クラスなので、99
として機能します また、「通常の」特性に加えて、109
よりも少し多くの作業を実行する必要があります。 バージョン。実際には非常に多くの作業が行われているため、次のメンバーを見るだけです:
- <リ>
118
:メンバー 121
を呼び出します または最大整数値へのフォールバック
131
:メンバー 145
を呼び出します または 159
へのフォールバック
164
:メンバー 175
を呼び出します またはメンバー関数 184
へのフォールバック 、そうでなければエラー
197
:メンバ typedef 207
に転送します または 210
を使用するフォールバック
セットアップ
224
のデフォルトの特殊化 インスタンス化される型の正確なプロパティに応じて、さまざまな実装を提供する必要があります。タグのディスパッチに関する投稿で学んだように、さまざまな実装は、タグを挿入して転送するだけの親関数を使用して、さまざまな関数に抽出する必要があります。 /P>
これは次のようになります:
namespace traits_detail
{
...
}
template <class RawAllocator>
class allocator_traits
{
public:
static std::size_t max_node_size(const allocator_type &state)
{
return traits_detail::max_node_size(/* tag object */, state);
}
static std::size_t max_array_size(const allocator_type &state)
{
return traits_detail::max_array_size(/* tag object */, state);
}
static void* allocate_node(allocator_type& state,
std::size_t size, std::size_t alignment)
{
return traits_detail::allocate_node(/* tag object */,
state, size, alignment);
}
using is_stateful = ...;
};
実装関数は詳細名前空間 235
にあります これらは純粋な実装の詳細であるため、選択するには適切なタグ タイプが必要です。
それを行う 1 つの方法は、型が必要なメンバー関数を持っているかどうかをチェックするミニ トレイトを記述することです。しかし、これは面倒なので、やめることにしました。
代わりに、ヒエラルキーに気付くことができます 実装では、最初にメンバー関数を呼び出そうとし、次に何かにフォールバックします。お見せしたように、これはタグの階層によってモデル化することもできます:
struct error {}; // for types without the member function
struct std_concept : error {}; // for types that provide the standard Allocator functions (allocate() instead of allocate_node())
struct min_concept : std_concept {}; // for types that provide only the minimal RawAllocator concept functions
struct full_concept : min_concept {}; // for types that provide the full set of functions
特性内の親関数は 246
型のオブジェクトを渡します 実装に対して、オーバーロードの解決により、階層内の最初の適切な実装が選択されます。
258
の実装
267
最も単純な関数です。メンバ関数 273
がある場合 、それを呼び出し、それ以外の場合は 286
型の最大値を返します .
これは次のように変換されます:
template <class Allocator>
std::size_t max_node_size(full_concept, const Allocator &alloc)
{
return alloc.max_node_size();
}
template <class Allocator>
std::size_t max_node_size(min_concept, const Allocator &) noexcept
{
return std::size_t(-1);
}
しかし、上記のコードは、派生から基底への変換を必要としないため、常に最初のオーバーロードを選択します!適切なメンバー関数のない型の場合、これはコンパイルに失敗します.したがって、最初のオーバーロードを無効にする方法が必要ですメンバー関数のない型。
そして、私の「オーバーロード解決の制御」シリーズのパート 4 を読んだことがあるなら、これはベルを鳴らすでしょう:SFINAE、つまり式 SFINAE を使用して、次のように最初のオーバーロードを無効にすることができます:
template <class Allocator>
auto max_node_size(full_concept, const Allocator &alloc)
-> decltype(alloc.max_node_size())
{
return alloc.max_node_size();
}
template <class Allocator>
std::size_t max_node_size(min_concept, const Allocator &) noexcept
{
return std::size_t(-1);
}
299
を入れることで 最後に、メンバー関数の存在が署名の一部になるため、それがない型ではテンプレート引数推定が失敗します。次に、他の候補を選択し、そのときだけ、派生先のために一致が悪いためです。 -塩基変換。
完璧です。
303
の実装
317
326
と非常によく似ています .フォールバックは 334
を返すことのみを必要とします 、しかし、メンバー関数の存在に依存しないように、フォールバック自体を持つバージョンを使用することを確認する必要があります.
これは次のように変換されます:
template <class Allocator>
auto max_array_size(full_concept, const Allocator &alloc)
-> decltype(alloc.max_array_size())
{
return alloc.max_array_size();
}
template <class Allocator>
std::size_t max_array_size(min_concept, const Allocator &alloc)
{
return max_node_size(full_concept{}, alloc);
}
もう退屈しているかもしれないので、シートベルトを締めて 340
と入力してください !
352
の実装
362
最初に 372
の呼び出しを試みます 、その後 383
にフォールバックします :
template <class Allocator>
auto allocate_node(full_concept, Allocator &alloc,
std::size_t size, std::size_t alignment)
-> delctype(alloc.allocate_node(size, alignment))
{
return alloc.allocate_node(size, alignment);
}
template <class Allocator>
auto allocate_node(std_concept, Allocator &alloc,
std::size_t size, std::size_t)
-> decltype(static_cast<void*>(alloc.allocate(size)))
{
return static_cast<void*>(alloc.allocate(size));
}
しかし、タイプが 390
を提供しない場合はどうなるでしょうか? メンバー関数か?
その後、オーバーロードの解決に失敗します。いずれかの関数を提供するには型が必要であり、それ以外の場合は使用してはならないため、これは理にかなっています.しかし、オーバーロード解決エラーは、最もきれいで簡潔な種類のエラーメッセージではありません.
ライブラリのユーザーが 408
を書いたときに大量のエラー メッセージを表示する代わりに 410
の代わりに 、正確な情報を示す簡潔なエラー メッセージがあればいいと思いませんか?
シリーズのパート 2 で示したように、これは実際に可能です。まず、426
をトリガーするフォールバック オーバーロードが必要です。 これは、438
を提供することによって実現されます。 テンプレート パラメーターに依存する値。最も洗練された方法は、メンバー定数を持つテンプレート化された構造体です。
まとめると:
template <typename T>
struct invalid_allocator_concept
{
static const bool error = false;
};
// new overload
template <class Allocator>
void* allocate_node(error, Allocator &,
std::size_t, std::size_t)
{
static_assert(invalid_allocator_concept<Allocator>::error,
"type does not provide: void* allocate_node(std::size_t, std::size_t)");
return nullptr; // to silence warning
}
現在もユーザーはエラー メッセージを受け取りますが、おそらくライブラリの奥深くにネストされている可能性がありますが、最初に有用で有益なエラー メッセージが表示されるため、ユーザーは顔をしかめてタイプミスを修正できます。
448
の実装
残っているのは typedef 451
だけです .しかし、適切なメンバ typedef を使用してテンプレートの特殊化を書き始める前に、ここで止めさせてください。
これにはオーバーロードの解決も使用できます。戻り値の型は実装ごとに変更でき、462
を介して typedef に格納できます。 オーバーロードの解決は、テンプレートの特殊化よりもはるかに簡単に制御できるため、強くお勧めします。
トレイトには次のものがあります:
using is_stateful = decltype(traits_detail::is_stateful<Allocator>(traits_detail::full_concept{});
実装は次のように行うことができます:
template <class Allocator>
auto is_stateful(full_concept)
-> decltype(typename Allocator::is_stateful{});
メンバー typedef のオブジェクトを作成し、その型を戻り値の型として使用します。関数が呼び出されることはないため、実装は必要ありません。
アロケータはステートフルであるため、そうでない場合、フォールバックはもう少し複雑です。 空なので、結果を逆にする必要があります:
template <class Allocator>
auto is_stateful(min_concept)
-> std::integral_constant<bool, !std::is_empty<Allocator>::value>
しかし、これは結果として得られるクラス テンプレートの特殊化よりもはるかに単純であり、簡単に拡張できます。
結論
この投稿では、特定のメンバー関数に単純に転送するのではなく、フォールバックを提供する特性クラスを作成しました。これにより、デフォルトの動作をオーバーライドするために使用できるオプションの関数を使用して、最小限の必要な概念が可能になります。
実装は、必要なメンバー関数がない場合に特定のオーバーロードを無効にする SFINAE を使用して、階層から特定のタグ型を取得するさまざまな実装関数を使用して行うことができます。Typedef は同じ方法で実装できます。コード>
完全な実装に興味がある場合 481
、こちらの github で見つけることができます。