C++11 での概念チェック

この投稿では、C++11 で概念チェックを行うために私が最近開発したいくつかのユーティリティについて説明します。これらのユーティリティは、範囲を再実装する進行中のプロジェクトの一部であり、これも C++11 用ですが、コンセプト チェック ユーティリティは、それ自体で便利で興味深いものだと思います。

コンセプト、これまでの物語

(概念が何であるかを既に知っている場合は、このセクションをスキップしてください。)

C++ での概念チェックの話は長く、非常に劇的です。それらは C++0x に追加され、熱く議論され、引き裂かれ (大量の白髪とともに)、手を絞められ、胸が殴られ、荒布が引き裂かれました… 本当に聖書的なものです。わかりました、そうではないかもしれませんが、そうだった 劇的。いずれにせよ、それらを元に戻すという新しい提案があるので、多くの人が悪いコンセプトを望んでいることは明らかです.

しかし、バックアップしましょう。 とは コンセプト?ある意味で、プログラマーは 1998 年またはそれ以前の標準テンプレート ライブラリが最初に登場したときから概念を使用してきました。おそらくイテレータとは何かを知っているでしょうし、 std::vector のようなランダム アクセス イテレータには違いがあることも知っています。 のイテレータ、および std::list のような双方向イテレータ の。 「ランダム アクセス イテレータ」や「双方向イテレータ」などは概念です。 .型は、ランダム アクセス反復子になるために特別な基本クラスから継承する必要はありません。特定の構文とセマンティクスをサポートする必要があるだけです。そして、ランダム アクセス イテレータの概念は改良版です。 双方向イテレータ;前者は後者のすべての構文とセマンティクス (例:インクリメントとデクリメント) に加えて、いくつかの追加機能 (例:イテレータを n だけ進めることができる) をサポートします。 位置は O(1) 時間です)。

概念により、多態的アルゴリズム (さまざまなタイプのオブジェクトを処理するアルゴリズム) を定義できます。そして、それらは非常に疎結合で高性能です。アルゴリズムが、概念によって約束された構文とセマンティクスのみに依存している場合、それはただ機能するはずです。そして、摩擦があります。現在、特定のアルゴリズムがランダム アクセス反復子を必要とすることをコードで示す方法はなく、双方向反復子を渡すと、最も不快な方法で確実に検出されます。したがって、言語自体に概念チェックを追加したいという願望があります。

コンセプト、新たな希望?

十分な裏話。コードを見せてね?これは、私のユーティリティで定義された反復子の概念の完全な改良階層です。

struct Iterator
  : refines<CopyConstructible, CopyAssignable,
            Destructible>
{
    // Valid expressions
    template<typename T>
    auto requires(T && t) -> decltype(
        concepts::valid_expr(
            *t,
            concepts::has_type<T &>(++t)
        ));
};

struct OutputIterator
  : refines<Iterator(_1)> // OutputIterator<T,U> refines
{                         // Iterator<T>
    template<typename T, typename O>
    auto requires(T && t, O && o) -> decltype(
        concepts::valid_expr(
            t++,
            *t = o,
            *t++ = o
        ));
};

struct InputIterator
  : refines<Iterator, Comparable>
{
    template<typename T>
    auto requires(T && t) -> decltype(
        concepts::valid_expr(
            t++,
            concepts::convertible(*t, *t++)
        ));
};

struct ForwardIterator
  : refines<InputIterator>
{
    template<typename T>
    auto requires(T && t) -> decltype(
        concepts::valid_expr(
            concepts::same_type(*t, *t++)
        ));
};

struct BidirectionalIterator
  : refines<ForwardIterator>
{
    template<typename T>
    auto requires(T && t) -> decltype(
        concepts::valid_expr(
            concepts::has_type<T &>( --t ),
            concepts::same_type(*t, *t--)
        ));
};

struct RandomAccessIterator
  : refines<BidirectionalIterator>
{
    template<typename T>
    auto requires(T && t) -> decltype(
        concepts::valid_expr(
            concepts::model_of<SignedIntegral>(t-t),
            t = t + (t-t),
            t = (t-t) + t,
            t = t - (t-t),
            t += (t-t),
            t -= (t-t),
            concepts::same_type(*t, t[t-t]),
            concepts::model_of<Orderable>(t)
        ));
};

これは一見すると少し奇妙に見えるかもしれないので、順を追って説明します。最初の 2 行…

struct Iterator
  : refines<CopyConstructible, CopyAssignable,
            Destructible>

Iterator という概念があるそうです CopyConstructible の概念を洗練する 、 CopyAssignable 、および Destructible .確かに、すべての反復子はこれらの基本的な操作をサポートする必要があります。定義したい概念が他の概念を洗練しない場合は、その部分を省略できます。

次の数行は、いわゆる有効な式について説明しています :すべての反復子がサポートする必要がある有効な構文:

template<typename T>
auto requires(T && t) -> decltype(
    concepts::valid_expr(
        *t,
        concepts::has_type<T &>(++t)
    ));

イテレータを逆参照してインクリメントできる必要があり、インクリメント操作の結果は T & 型である必要があります .これはすべてに当てはまります イテレータ。コンセプトの有効な式を定義するときは、上記のパターンに従ってください:a requires 右辺値参照によって 1 つ以上のオブジェクトを受け取るメンバー関数、および decltype(concepts::valid_expr(/*...*/)) の末尾の戻り値の型 あなたの有効な表現で。コンセプトの定義はこれで終わりです。 has_type のようなユーティリティがいくつかあります 、 same_type 、および model_of コンセプトチェックのようなものですが、それらはすべて詳細です.

コンセプト チェック

概念の定義がどのように見えるかを見てきました。次に、それらの使用方法を見てみましょう。上記のすべての定義が concepts にあると想像してください 名前空間。概念定義に対して特定の型をテストするためのヘルパーをいくつか定義しましょう。それらは次のようになります:

template<typename T>
constexpr bool Iterator()
{
    return concepts::models<concepts::Iterator, T>();
}

template<typename T, typename O>
constexpr bool OutputIterator()
{
    return concepts::models<concepts::OutputIterator, T, O>();
}

template<typename T>
constexpr bool InputIterator()
{
    return concepts::models<concepts::InputIterator, T>();
}

template<typename T>
constexpr bool ForwardIterator()
{
    return concepts::models<concepts::ForwardIterator, T>();
}

template<typename T>
constexpr bool BidirectionalIterator()
{
    return concepts::models<concepts::BidirectionalIterator, T>();
}

template<typename T>
constexpr bool RandomAccessIterator()
{
    return concepts::models<concepts::RandomAccessIterator, T>();
}

これらの概念チェッカーが constexpr であることに注意してください ブール関数。 concepts::models 関数は、指定された型が概念をモデル化する場合は true を返し、そうでない場合は false を返します。簡単。また、私はマクロが嫌いなので、これまでマクロを 1 つも使用していないことに注意してください。

特定の型が概念をモデル化するかどうか疑問に思っている場合、コンパイル時のブール値として答えを得ることができます。たとえば、std::advance のようなものを書いているとします。 アルゴリズム。 2 つの引数がそれぞれ入力反復子と整数型であることを確認したい場合:

template<typename InIt, typename Diff>
void advance(InIt & it, Diff d)
{
    static_assert(ranges::Integral<Diff>(),
                  "Diff isn't integral");
    static_assert(ranges::InputIterator<InIt>(),
                  "InIt isn't an input iterator");
    // ...
}

マクロにアレルギーがない場合は、次のこともできます:

template<typename InIt, typename Diff>
void advance(InIt & it, Diff d)
{
    CONCEPT_ASSERT(ranges::Integral<Diff>());
    CONCEPT_ASSERT(ranges::InputIterator<InIt>());
    // ...
}

(ご覧のとおり、私のコードでは、概念チェック機能はすべて ranges にあります 名前空間です。) これはかなりいいです。誰かが advance に電話したら 間違った型を使用すると、適切なエラー メッセージが表示されます。しかし、多分あなたは何か他のものを望んでいます。おそらく advance がたくさんあります 関数であり、型が概念をモデル化していない場合は、このオーバーロードが静かに消えるようにする必要があります。次に、これを行うことができます:

template<typename InIt, typename Diff,
         typename = concepts::requires_t<
                        ranges::Integral<Diff>() &&
                        ranges::InputIterator<InIt>()>>
void advance(InIt & it, Diff d)
{
    // ...
}

これは SFINAE を使用して advance を作成します コンセプト要件が満たされない場合、機能は消滅します。それは機能しますが、少し醜くなっています。鼻をつまんでマクロを使った方がいいかもしれません:

template<typename InIt, typename Diff,
         CONCEPT_REQUIRES(ranges::Integral<Diff>() &&
                          ranges::InputIterator<InIt>())>
void advance(InIt & it, Diff d)
{
    // ...
}

私はマクロが嫌いですが、それでも大丈夫です。

コンセプトベースのオーバーロード

std::advanceについて何か知っているなら 、なぜ私がそれを例として選んだか知っているかもしれません。 advance 進歩 特定の数の位置による反復子。ほとんどのイテレータは前にバンプする必要があります n 回、遅いです。ただし、反復子がランダム アクセスの場合は、n を追加するだけです。 それに、完了します。新しいコンセプト チェック ユーティリティを使用して、どのようにそれを達成しますか?

C++98 では、これはイテレータ タグ タイプとタグ ディスパッチによって実現されます。残念ながら、タグのディスパッチは、C++11 で実行できる最善の方法です。そのため、言語機能が本当に必要です。しかし、私のコードでは、かなり簡単になります。概念定義自体をタグとして使用できます。見てみましょう。

答える最初の質問は、指定されたイテレータ型で、最も洗練されたものは何かということです それがモデル化するイテレータの概念? int* のような型の場合 RandomAccessIterator である必要があります 、ただし std::list::iterator の場合 BidirectionalIterator である必要があります . most_refined_t というユーティリティを使用して、その情報を取得できます。 .ここでは most_refined_t を使用します iterator_concept_t を実装する イテレータ型がモデル化する概念を示すエイリアス:

template<typename T>
using iterator_concept_t =
    concepts::most_refined_t<
        concepts::RandomAccessIterator, T>;

most_refined_t concepts::RandomAccessIterator をルートとする絞り込み階層の幅優先検索を行います 、タイプ T によってモデル化された最も洗練された概念を探します .これを使用して advance を最適に実装する方法を次に示します。 :

// Random-access iterators go here
template<typename RndIt, typename Diff>
void advance_impl(RndIt & it, Diff d,
                  ranges::concepts::RandomAccessIterator)
{
    it += d;
}

// All other iterator types go here
template<typename InIt, typename Diff>
void advance_impl(InIt & it, Diff d,
                  ranges::concepts::InputIterator)
{
    for(; d != 0; --d)
        ++it;
}

template<typename InIt, typename Diff,
         CONCEPT_REQUIRES(ranges::InputIterator<InIt>() &&
                          ranges::Integral<Diff>())>
void advance(InIt it, Diff d)
{
    advance_impl(it, d, ranges::iterator_concept_t<InIt>{});
}

ご覧のとおり、概念ベースのオーバーロードは、特定の型がモデル化する概念に基づいて適切な実装にディスパッチすることによって実現されます。これはすべて、概念定義に基づいて機能します。思い出すと、洗練と有効な式を宣言的に指定するだけで済みます。個別のタグ、特性、またはメタ関数を定義する必要はありませんでした。ぼろぼろではありません。

足りないものは?

このパズルに欠けている大きなピースは、require 句に対してアルゴリズムを自動的にチェックする機能です。 advance が アルゴリズムは言う 入力反復子のみが必要です。しかし、その実装が実際に別の前提を置いている場合はどうなるでしょうか?仮定を満たさない型でアルゴリズムを呼び出そうとするまではわかりません。残念ながら、これが最新の状態であり、私にできることは何もありません。申し訳ありません。

抽象化を具体化する

私の概念チェック ライブラリは完璧ではありません。これは、真の言語サポートがどのようなものになるかについての淡い概算です。一体、それはまだ図書館でさえありません。しかし、これまでの範囲コードでこのユーティリティを使用した私の限られた経験では、実際の利点があります。豊富なオーバーロード セットを作成し、型がモデル化する必要がある概念を宣言するだけで、どのオーバーロードが選択されるかを調整できます。また、概念を定義するのは簡単です。楽しいです。汎用コードを書くときに、実際に期待どおりの動作が得られるという自信が持てます。

それで、よろしければ、あなたの考えをコメントに残してください。これは役に立ちますか?これが進むのを見てみたい方向はありますか?やってみようかな (十分な空き時間に ) これを適切なライブラリに変換するには、おそらく Boost.Concept_check の最新の代替品として?あなたの考えを教えてください。

参考までに、(ひどくコメント不足で文書化されていない) コードをここで見つけることができます。

x