実装の課題:C++14 の概念

C++17 に概念を含めるための技術仕様である TS という概念があります。C++ では、概念は常に … 概念でした。テンプレート パラメータの制約を文書化するために使用されます。例:

template <typename RandomAccessIterator, typename Comperator>
void sort(RandomAccessIterator begin, RandomAccessIterator end, Comperator comp);

この関数には 09 という要件があります と 18 ランダムアクセス反復子と 23 の両方です は比較関数です。現在、概念は文書化されているだけであり、それらを無視すると大きなエラー メッセージが表示されます。概念 TS は、それらを言語に直接埋め込む方法を提供し、たとえば、概念に基づくオーバーロードをより簡単にします。

しかし、それは実際に言語に新しいものをもたらすわけではありません。それが行うことはすべて、今日の C++11 の式 SFINAE で達成できますが、(ほぼ間違いなく) よりクリーンな構文とより複雑な言語を言語にもたらすだけです.

この投稿では、C++14 言語機能のみを使用して概念を実装する方法を紹介します。非常に簡単に使用できるいくつかのライブラリ ユーティリティを紹介することで、できるだけ簡単に実装できるようにします。

課題

簡単に言うと、TS の概念は次の 2 つの機能を提供します。

<オール> <リ>

要件を指定してコンセプトを定義する能力

<リ>

テンプレート パラメーターに特定の概念を要求する機能。これはオーバーロードにも影響します。型が必要な概念を満たさない場合、別のオーバーロードが選択されます。

省略されたテンプレート構文など、より多くの機能も含まれていますが、これらの純粋な構文機能は無視しましょう。

概念の定義は次のようになります:

template <typename T>
concept bool my_concept = some-value;

まあ、それは既存のコードに書くのは簡単です:

template <typename T>
constexpr bool my_concept = some-value;

ほら、ただ 37 を使ってください 42 の代わりに 、完了。

より便利な部分は 53 です .62 基本的に、式がコンパイルされるかどうかをチェックするために使用されます。コンパイルされると、74 が返されます。 、それ以外の場合は 85 .

次のように使用できます:

template <typename T>
concept bool has_foo = requires(T t) {t.foo()};

98 - 変数 102 が与えられた場合、 true になります タイプ 112 の - 式 124 コンパイルされます。また、式の結果の型と、それがスローされるかどうかを確認することもできます:

requires(T t)
{
 { t.foo() };
 { t.bar() } noexcept -> int;
};

いくつかの 136 が与えられました 145 同様にコンパイルする必要があります。151 です。 164 に変換可能なものを返します .もちろん、上部にさまざまなタイプのパラメーターを追加できます。

177 もあります 条項 2.:テンプレート パラメーターから特定のものを要求するために使用されます。次のように使用できます:

template <std::size_t I>
void foo() requires I > 0;

現在 181 195 の場合にのみインスタンス化されます 204 より大きい .それ以外の場合、オーバーロードの解決は検索を続けます (他に何もない場合は失敗します)。

212 もちろん、句は定義済みの概念でも使用できます:

template <typename T>
void foo(T t) requires has_foo<T>;

これには、228 の上記の概念が必要です .簡略化できます:

template <has_foo T>
void foo(T t);

さらに:

void foo(has_foo t); // implictly a template

そのため、概念は通常、型とは異なる名前が付けられます。

239 式と句は TS の概念の 2 つの主な機能であり、それ以外はすべてシンタックス ハニーです。では、それらを実装する方法を見てみましょう。

240

うまくいく最初の試み

式がコンパイルされるかどうかを確認する方法が必要です。式 SFINAE のおかげで、これは驚くほど簡単です。たとえば、メンバー関数 259 を確認する方法は次のとおりです。 :

template <typename ... Ts>
using void_t = void;

template <typename T, typename AlwaysVoid = void_t<>>
struct has_foo : std::false_type {};

template <typename T>
struct has_foo<T, void_t<decltype(std::declval<T>().foo())>> : std::true_type {};

ここで重要なのは、非常に馬鹿げたエイリアス テンプレート 260 です。 .タイプに関係なく、常に 277 です .しかし、この小さなエイリアスは信じられないほど強力です.

クラステンプレート 286 があります 297 のいずれかにマップされます または 303 、タイプ 316 かどうかに応じて メンバ関数 326 を持っています .汎用テンプレートは 339 にマップされます .特殊化の順序規則のおかげで、コンパイラは可能な限り最も特殊化されたバージョンを選択しようとし、他のテンプレートを使用できない場合にのみジェネリック テンプレートをフォールバックとして使用します。 349 、これは専門化の選択を制御するキーです。

特殊化は、2 番目のタイプが 350 の場合に適用されます .2 番目のタイプが デフォルト であるため 368 へ 、これは常に当てはまります!しかし、 373 への引数 386 です 式。コンパイラは式を評価し、395 に渡す必要があります。 使用されなくても、式を評価するために、402 の呼び出しの戻り値の型を把握する必要があります。 いくつかの 418 で オブジェクト。

メンバー関数 428 を持つ型を渡す場合 、コンパイラは最初に特殊化を試み、すべての引数を評価します - 432 を含みます したがって、448459 の戻り型を検出できます

型にメンバー関数 461 がない場合 、コンパイラは戻り値の型を判別できません。これは置換エラーですが、ありがたいことにエラーではありません。

代わりに、コンパイラはさらに調べてメイン テンプレートを選択します。これは、同等の 473 とまったく同じです。

より一般的にする

しかし、それは非常に冗長です。

より良い方法は、一般的な 487 を作成することです 式を入れるだけでよい特性です。では、そうしましょう:

template <typename ... Ts>
using void_t = void;

template <typename T, template <typename> class Expression, typename AlwaysVoid = void_t<>>
struct compiles : std::false_type {};

template <typename T, template <typename> class Expression>
struct compiles<T, Expression, void_t<Expression<T>>> : std::true_type {};

トレイトで式をハードコーディングする代わりに、それを追加のテンプレート テンプレート パラメータとして渡します。形式が正しくない場合はインスタンス化してはならないため、テンプレート自体である必要があります。それ以外の場合は、まったく同じことを行い、式を評価しますSFINAE を許可する特殊化。

今度は 490 次のようになります:

template <typename T>
using use_foo = decltype(std::declval<T>().foo());

template <typename T>
using has_foo = compiles<T, use_foo>;

より複雑な例

特に、ほとんどの場合、そのような単純な概念がなく、必要なボイラープレートが少ないため、これははるかに冗長ではありません.たとえば、ここに私の 508 の説明があります コンセプト:

template <class Allocator>
concept bool BlockAllocator = requires(Allocator a, const Allocator ca, memory::memory_block b)
{
 {a.allocate_block()} -> memory::memory_block;
 {a.deallocate_block(b)};
 {ca.next_block_size()} -> std::size_t;
};

上記の手法を使用すると、次のようになります。

template <typename T>
struct BlockAllocator_impl
{
 template <class Allocator>
 using allocate_block = decltype(std::declval<Allocator>().allocate_block());

 template <class Allocator>
 using deallocate_block = decltype(std::declval<Allocator>().deallocate_block(std::declval<memory::memory_block>());

 template <class Allocator>
 using next_block_size = decltype(std::declval<const Allocator>().next_block_size());

 using result = std::conjunction<
 compiles_convertible_type<T, memory::memory_block, allocate_block>,
 compiles<T, deallocate_block>,
 compiles_same_type<T, std::size_t, next_block_size>
 >;
};

template <typename T>
using BlockAllocator = typename BlockAllocator_impl<T>::result;

2 つの 515 および 520 535 の単純な拡張です 544 をアサートする特性 タイプまたは 550 式の型。それらの実装は、読者の課題として残されています。

これらを使用すると、概念を実際に定義するのは簡単です。必要なすべての式のリストを作成し、それらをコンパイルする必要があります。追加の 566 を使用しました 式が外側のスコープに漏れないようにします。

さらに簡潔にしますか?

確かに、これは 579 よりもさらに冗長です。 バージョンですが、それほど悪くはありません。特に、ほとんどの場合使用しているためです 概念を書くのではなく、概念を書く必要があることはめったにありません。

私を本当に悩ませている唯一のことは、587 を常に使用していることです。 .このようなものがうまくいくなら、もっといいでしょう:

template <class Allocator>
using deallocate_block = decltype([](Allocator& a, memory::memory_block b)
 {
 return a.deallocate_block(b);
 } (std::declval<Allocator&>(), std::declval<memory::memory_block>()));

しかし、ラムダは未評価のコンテキストに現れてはなりません。仮にあったとしても、意図したとおりに機能するかどうかはよくわかりません.

とにかく、概念を定義して 598 をエミュレートできるようになりました 式、603

618

620 句は単なる 639 です :

template <typename ResultType, typename CheckType, template <typename> class ... Values>
using requires = std::enable_if_t<std::conjunction<Values<CheckType>...>::value, ResultType>;

エイリアス テンプレートを使用してより強力にし、任意の数の概念を使用して一度にチェックできるようにします。

template <typename T>
auto foo(const T& t) -> requires<void, T, ConceptA, ConceptB>;

642 を使用した場合 前に、フォールバックを選択する場合は、すべてのオーバーロードに配置する必要があることを知っています。そのため、別のヘルパー エイリアスを定義しましょう:

template <typename ResultType, typename CheckType, template <typename> class ... Values>
using fallback = std::enable_if_t<std::conjunction<std::negation<Values<Check>>...>::value, ResultType>;

652 すべての条件が false の場合にのみ有効です。これを使用すると、複数のコンセプトを簡単にディスパッチできます:

template <typename T>
auto func(const T& t) -> requires<void, T, ConceptA>;

template <typename T>
auto func(const T& t) -> requires<void, T, ConceptB>;

template <typename T>
auto func(const T& t) -> fallback<void, T, ConceptA, ConceptB>;

他のすべての条件をフォールバック関数に入れる必要があることに注意してください。

インライン コンセプトの定義

概念を事前に定義する必要がなく、1 つの場所でのみ使用する必要がある場合は、666 も使用できます。 直接:

template <typename T>
auto func(const T& t) -> void_t<decltype(t.foo())>;

この関数は 670 の場合にのみ選択されます メンバー関数 685 を持っています .ほとんどの場合、これで十分です。

結論

693 のエミュレーション 703 とほぼ同じ構文を使用して句が可能です .テンプレートを実際に表示しない「よりかわいい」構文を作成する必要はありません:

void func(const ConceptA& a); // template if `ConceptA` is a real concept

長い形式は、上記のソリューションとほぼ同じです:

template <typename T>
void func(const T& t) requires ConceptA<T>;

標準委員会は言語ソリューションよりもライブラリ ソリューションを好んだと思いましたか?では、なぜライブラリ ソリューションを言語の一部にするのでしょうか?

715 ただし、より冗長な構文とライブラリの追加の助けを借りてのみエミュレートできます。使用するたびに記述する必要があります。

ライブラリの基礎 v2 TS に既に含まれていることを除いて。私が示したイディオムは検出イディオムで、同様に 728 を提案しています .

しかし、その助けを借りても、構文は 731 ほど良くありません。 しかし、追加の複雑さはそれだけの価値がありますか?

つまり、それは構文をより良くしますが、それに直面しましょう:概念を書くのは誰ですか?

テンプレートの重いライブラリ。また、TMP を他の目的で使用する必要があります。1 つの部分を単純化するだけの価値はありますか?

すべての新機能、特に言語機能には、複雑さが増し、実装が難しくなり、学習が困難になります。C++ はすでに肥大化した言語です。新しい構文糖衣が本当に必要なのでしょうか?他のもののルールを弱めることで同じことを達成できないでしょうか?私が示したラムダの例のように?

幸いなことに、私は決定を下す必要はありませんが、それについて怒鳴ることはできます.いつかコンセプトが C++ になるとしたら、古いコンパイラをサポートする必要がないプロジェクトでそれらを使用するでしょう.しかし、これは楽しみにしている言語機能です。

付録 A:しかし、概念はエラー メッセージを改善します!

手始めに:私の知る限り、そうではありません。これは今では変更されている可能性があります。

しかし、エラー メッセージの改善については既に書いています。文字通り同じ手法をここで適用できます。

template <typename T>
auto func(const T& t) -> requires<void, T, ConceptA>;

template <typename T>
auto func(const T& t) -> fallback<void, T, ConceptA>
{
 static_assert(always_false<T>::value, "T does not model ConceptA");
}

745 の場合 753 をモデル化していません 、フォールバックが選択され、静的アサーションが失敗し、ユーザー定義のエラー メッセージが表示されます。

付録 B:765 について ?

概念の代わりに、言語は 779 に焦点を当てるべきだと時々言われます .

オーバーロードを選択する代わりに、C++17 の 788 を使用して関数の実装を選択することもできます .これにより、ケースごとに有効な実装がある場合、ディスパッチ メカニズムとしての概念が不要になりますが、それでも式を検出するための特性が必要です。

ただし、それぞれのケースに有効な実装がない場合は、SFINAE でそれをさらに検出し、別のオーバーロードを使用する必要があるかもしれません。

[meta] 付録 C:広告があります!

はい、現在このサイトに広告があります。

Google Adsense に申し込みましたが、実際に受け入れられるとは思っていませんでした.まあ、今は受け入れています.

これらの広告は主に、私がより多くの投稿を書くための動機として使用されます。私はそれで多くのお金を稼ぐことは期待していません.とにかく、ほとんどの人は広告ブロッカーを使用しています.