C++20 の概念は構造的です:何を、なぜ、どのように変更するのでしょうか?

C++20 は、言語機能として概念を追加しました。それらは、Haskell の型クラス、Rust のトレイト、または Swift のプロトコルとよく比較されます。

しかし、それらを際立たせる特徴が 1 つあります。型は C++ の概念を自動的にモデル化します。Haskell では、06 が必要です。 、Rust では、13 が必要です 、Swift では 24 が必要です .しかし、C++ では? C++ では、概念は整形式の構文をチェックする凝ったブール述語にすぎません。構文を整形式にするすべての型が述語を渡し、概念をモデル化します。

これは正しい選択でしたが、場合によっては希望どおりにならないこともあります。さらに詳しく調べてみましょう。

公称と構造の概念

型システムから用語を採用するために、C++20 の概念では structural を使用します 型付け:型が概念に必要な構造と同じ構造を持っている場合、型は概念をモデル化します。必要な式があります。対照的に、型クラス、特性、およびプロトコルはすべて nominal を使用します 型付け:型は、ユーザーがそれを示す宣言を記述した場合にのみ、概念をモデル化します。

たとえば、39 をチェックする C++ の概念を考えてみましょう。 と 46 :

template <typename T>
concept equality_comparable = requires (T obj) {
  { obj == obj } -> std::same_as<bool>;
  { obj != obj } -> std::same_as<bool>;
};

これは 56 をモデル化する型を書く方法です C++20 の構造概念:

// Define your type,
struct vec2
{
    float x, y;

    // define the required operators,
    friend bool operator==(vec2 lhs, vec2 rhs)
    {
        return lhs.x == rhs.x && lhs.y == rhs.y;
    }

    // operator!= not needed in C++20 due to operator rewrite rules!
};

// ... and that's it!
static_assert(equality_comparable<vec2>);

対照的に、これは 65 をモデル化する型を記述する方法です。 名目上の概念を持つ架空の C++20 で:

// Define your type
struct vec2 { … }; // as before

// ... and tell the compiler that it should be `equality_comparable`.
// Most languages also support a way to define the operation here.
concept equality_comparable for vec2;

公称の方が良い…

私の意見では、名目上の概念は構造上の概念よりも優れています:

<オール> <リ>

構造的な概念では、概念間のセマンティックの違いは許容されません。これは「構造」の一部ではないためです。

標準ライブラリの概念 78 を検討してください;述語型 88 については真です 型 92 間の二項関係を記述するもの および 106 :

template <typename F, typename ... Args>
concept predicate
    = /* F can be invoked with Args returning bool */;

template <typename R, typename T, typename U>
concept relation = predicate<R, T, T> && predicate<R, U, U>
                && predicate<R, T, U> && predicate<R, U, T>;

二項関係は広範な数学用語であるため、特定のプロパティとの関係が必要になることがよくあります。たとえば、111 などです。 ソートを制御する関数を受け取ります。これは特別な関係である必要があります:厳密な弱い順序です。幸いなことに、標準ライブラリの概念 122 があります。 :

template <typename R, typename T, typename U>
concept strict_weak_order = relation<R, T, U>;

ただし、それはただの 131 です ! 144 を使用するかどうか または 154 テンプレートパラメータ 167 を呼び出すのと同じくらい違います .これは単なる空想的なコメントです。コンパイラは気にしません。

C++ 型システムで表現できないセマンティックな違いは、構造的な概念でも表現できません。名目上の概念では、関数オブジェクトは 171 に明示的にオプトインする必要があります。 、これにより 2 つを区別できます。

<リ>

構造的な概念では、関数の名前は非常に重要です (皮肉なことに、私は知っています)。何らかの方法で標準ライブラリ (または概念を使用する他のライブラリ) と対話するコードを記述する場合は、同じ命名規則に従う必要があります。<のような名前コード>181 または 193 または 205 は本質的にグローバルに予約されており、標準ライブラリの概念が意図するものを意味する必要があります。

class TShirt
{
public:
    enum Size
    {
        small,
        medium,
        large
    };

    // The size of the T-Shirt.
    Size size() const;

    // The text on the front of the T-Shirt.
    const std::string& front() const;
    // The text on the back of the T-Shirt.
    const std::string& back() const;
};

214 上記のクラスは、220 のようなシーケンス コンテナーと間違われる可能性があります。 対応する概念の構文チェックに合格するためです。しかし、名目上の概念では、明示的にオプトインする必要があります。作成者が意図していない場合、型は名義上の概念をモデル化しません。

<リ>

反対に、コンセプトを概念的にモデル化しているが、必要なメソッドに異なる名前を使用している場合、それは機能しません。名前が重要であるためです。

233 とします。 上から 246 をオーバーロードしませんでした 代わりに関数 250 を提供しました :

struct vec2
{
    float x, y;

    bool is_equal(vec2 rhs) const
    {
        return x == rhs.x && y == rhs.y;
    }
};

型は等値比較可能ですが、264 ではありません – 名前は重要です。名目上の概念では、概念にオプトインする宣言は、通常、必要な関数の実際の実装を指定する方法も提供します。これにより、既存の型を他のインターフェースに簡単に適応させることができます:

// Dear compiler, vec2 models equality_comparable and here's how:
concept equality_comparable for vec2
{
    bool operator==(vec2 lhs, vec2 rhs)
    {
        return lhs.is_equal(rhs);
    }
}

そこで導入された名前は、その概念に限定されていると想像できます:それらは型自体にメンバーを追加せず、代わりに 273 を必要とする汎用コードでのみ使用できます。 タイプ。

…しかし構造は C++ が必要とするものです

では、名目上の概念の方が優れていると考えるのであれば、なぜ構造概念が C++ の正しい選択であると序文で述べたのでしょうか?構造概念には大きな利点が 1 つあります。それは、概念の前に記述されたコードに直面したときに便利だからです!>

C++20 で概念化されたすべての関数で、概念を明示的にオプトインする必要がある場合を想像してみてください。281 は使用できません。 コンテナー、イテレーター、型などのダミーの宣言を作成するまでは…移行は悪夢です!概念が自動的にモデル化されていれば、はるかに簡単です。

別の利点は、ライブラリの相互運用性です。3 つのライブラリ A、B、および C があり、A には概念があり、B には概念をモデル化する型があり、C はその 2 つを使用する場合、C は B の型を期待する関数に渡すだけで済みます。 B が A または C に依存する必要がない A の概念。実際にそれらを定義するライブラリをプルすることなく、概念に準拠する型を記述できます。これは、大きな依存関係を避けたい場合に便利ですが、それでもコードをシームレスに動作させることができます。

最後に、命名規則があまりにも普遍的に受け入れられているため、誰も敢えてそれから逸脱することはありません – 演算子を考えてみてください.コピー代入でコピーを行わない場合、または移動コンストラクターが移動しない場合、あなたの型は悪いです.したがって、 297 のような概念を持つことは完全に理にかなっています 自動的にモデル化されます。

3 つの利点すべてが「新しい」言語、つまり概念が最初から含まれている言語には当てはまらないことに注意してください。

  • 新しい言語にはレガシー コードがないため、型モデルのすべての概念に注釈を付けるための移行コストはかかりません。
  • 新しい言語は標準のパッケージ マネージャーを提供できるため、モデルの概念への依存を回避する必要が少なくなります。
  • 演算子のオーバーロードとその存在をチェックする概念を使用する代わりに、それをひっくり返すことができます:提供する概念を定義します 演算子のオーバーロード; 概念にオプトインする型は、対応するオーバーロードされた演算子を取得します。

そのため、Haskell、Rust、および Swift の決定は完全に理にかなっています。

ただし、ライブラリの完全に新しい概念を考案したり、セマンティクスに基づいて異なる概念を実際に区別する必要がある場合は、「ファンシー コメント」だけではなく、C++ で名目上の概念が必要になる場合があります。

それで、あなたは何をしますか?

C++20 の公称概念

インターフェースは同じだがセマンティクスが異なる概念を区別する問題は、C++98 – イテレーターにまでさかのぼります。はなくなり、古い値を取り戻すことはできません。前方反復子を使用すると、それをコピーして古い値を保持できます。

template <typename InputIterator>
void handle_input(InputIterator begin, InputIterator end)
{
    …

    auto a = *begin;

    auto copy = begin;
    ++begin;
    auto b = *begin;

    …

    auto c = *copy;
    assert(c == a); // ups, c is actually the same value as b!
}

では、入力反復子と前方反復子をコードで区別するにはどうすればよいでしょうか?簡単です:それらを区別する構文をいくつか追加します。

イテレータの場合、すべてのイテレータに 309 が関連付けられています 何かが入力反復子であるかどうかを明示的に示す typedef (319 ) または前方イテレータ イテレータ (328 ).実際、C++98 は型のインターフェイスを検出し、それに基づいてオーバーロードを行うのにあまり適していなかったため、すべてのイテレータ カテゴリにイテレータ カテゴリがあります…

ただし、タグ型を使用してセマンティック プロパティを区別するという基本的な考え方は、新しい C++20 イテレータの概念のために維持されました。必要な typedef は、339 と呼ばれるようになりました。 理由がありますが、346 も検索します .

テクニック #1 :他の点では同一の概念を区別するダミーの typedef のような追加の構文を追加します。

// concept definition ===//
template <typename T>
concept my_concept
  = requires { typename T::my_concept_tag; }
  && …;

//=== concept modelling ===//
struct my_type_modelling_the_concept
{
    using my_concept_tag = void; // Doesn't matter.
};

別のケースは 355 の違いです と 367 .A 372 385 です (開始/終了のあるもの) これも移動可能ですが、移動およびコピー操作 (提供されている場合) は一定時間で発生します。したがって、決定的に 395 408 ではありません :begin/end があり、移動可能 (さらにはコピー可能) ですが、コピー操作は O(1) にはありません!そのため、 419 429 ではありません – 同じ構文を持っているため、これもコンパイラーによって検出することは不可能です.

437 をモデル化するには 型は、変数テンプレート 442 を特殊化してオプトインする必要があります 455 に設定するには :

namespace my_namespace
{
    class MyViewtype
    {
    public:
        iterator begin() const;
        iterator end() const;
    };
}

namespace std
{
    // Tell the compiler that your view is a view.
    template <>
    constexpr bool enable_view<my_namespace::MyViewType> = true;
}

これを 464 と比較すると 上記の公称概念の例では、基本的に同じように見えることに気付くでしょう!型の構文要件を正式に満たし、概念をモデル化したいことを示す追加の宣言を記述します.コア言語ではなく、ライブラリです。

ただし、471 の特殊化 面倒です (現在の名前空間を閉じて、名前空間 487 を開きます) 、 498 を書きます 、…)、オプトインする簡単な方法もあります。502 から継承するだけです。 .

namespace my_namespace
{
    // Tell the compiler that your view is a view.
    class MyViewtype : public std::view_base
    {
    public:
        iterator begin() const;
        iterator end() const;
    };
}

これは、仮想関数や CRTP (ビュー用の CRTP 基本クラスもありますが) などの継承ではありません:517 528 の特殊化されていないバージョンでチェックできる構文要件を提供できるのは、単なる空の型です。 :

namespace std
{
    struct view_base
    {};

    // By default, a type is a view iff it inherits from view_base.
    template <typename T>
    constexpr bool enable_view = std::is_base_of_v<view_base, T>;
}

テクニック #2 :変数テンプレートの特殊化および/またはタグ タイプからの継承により、概念を有効にします

//=== concept definition ===//
struct my_concept_base {};

template <typename T>
constexpr bool enable_my_concept
  = std::is_base_of_v<my_concept_base, T>;

template <typename T>
concept my_concept = enable_my_concept<T>
  && requires (T obj) { … };

//=== concept modelling ===//
struct my_type_modelling_the_concept : my_concept_base
{
  …
};

変数テンプレートによって追加された追加の間接層は、一部の型が 531 をモデル化したい場合にのみ必要です。 ただし、543 から継承することはできません (非クラス型、既存の型)。クラスによってのみモデル化されるまったく新しい概念を追加する場合は、557 を使用できます。

私は「タグ型から継承して概念を有効にする」イディオム (EACBIFATT?) が本当に好きです。これは、オプトインするための最小限の構文オーバーヘッドで公称概念を提供します。基本クラスを拡張して、オプション機能のデフォルト実装を注入することもできます。単純な名前の非表示によって「オーバーライド」できます。

ここで、疑問に思うかもしれません:ユーザーが何かを明示的に継承する必要がある場合、それを単独で使用して関数を制約しないのはなぜでしょうか?結局のところ、C++98 以降、反復子に対して機能していました。

ただし、型が概念をモデル化すると主張しているが、実際にはそうではない場合を考えてみましょう。追加の構文チェックを使用すると、関数を呼び出そうとするとエラー メッセージが表示されます。概念がなければ、内部のどこかにあります。コードは型を使用しようとします。

それが価値があるかどうかはあなた次第です.基本クラスの存在のみを使用してください。

逆名義概念

反対に、概念を明示的にオプトインするのではなく、オプトアウトしたい場合もあります。

たとえば、569 577 です 584 で サイズを定数時間で返す関数.繰り返しになりますが、これはコンパイラによって検証できないため、追加の公称チェックが必要です.再びEACBIFATTをスローできますが、これは面倒です:ほとんどの 599 関数は O(1) です。

代わりに、ロジックが逆になります:604 を特殊化してオプトアウトしない限り、デフォルトでは、構文要件を満たす場合、型は概念をモデル化します。 .

namespace std
{
    // MyLinkedList has O(n) size.
    template <typename T>
    constexpr bool disable_sized_range<MyLinkedList<T>> = true;
}

テクニック #3 :変数テンプレートを特殊化することにより、概念を明示的に無効にします

template <typename T>
constexpr bool disable_my_concept = false;

template <typename T>
concept my_concept = !disable_my_concept<T>
  && requires (T obj) { … };

継承するタグ タイプを再び提供することもできますが、何かを継承してオプトアウトするのは奇妙に思えることに注意してください。

結論

C++20 の概念は、構文に基づいて自動的にモデル化されます。セマンティクスは気にしません。

そのため、セマンティクスが異なる同一の構文を区別したい場合は、それを区別するための構文を導入する必要があります。良い方法は、基本クラスの存在を確認することです。タイプは、それを継承することで簡単にオプトインできます。 typedef または変数の特殊化を追加することもできます。同じアプローチを使用して、概念をオプトアウトすることもできます。