C ++ 20標準:新しいC ++機能の概要。パート3「コンセプト」





2月25日、Yandexのコース 「C ++開発者」の作成者。実践的な作業であるG​​eorgyOsipovが、C ++言語の新しい段階であるC ++ 20標準について話しました。講義では、規格のすべての主要な革新の概要を説明し、それらを今すぐ適用する方法と、それらがどのように役立つかについて説明します。 ウェビナーを



準備する際 目標は、C ++ 20のすべての主要機能の概要を説明することでした。したがって、ウェビナーは豊富で、ほぼ2.5時間続きました。便宜上、テキストを6つの部分に分割しました。



  1. モジュールとC ++の簡単な歴史
  2. 操作「宇宙船」
  3. コンセプト。
  4. 範囲。
  5. コルーチン。
  6. その他のコアおよび標準ライブラリ機能。結論。


これは、最新のC ++の概念と制限をカバーする3番目の部分です。



コンセプト







動機



ジェネリックプログラミングはC ++の重要な利点です。私はすべての言語を知っているわけではありませんが、このレベルでそのようなものを見たことがありません。



ただし、C ++でのジェネリックプログラミングには大きな欠点があります。発生するエラーは、苦痛です。ベクトルをソートする単純なプログラムを考えてみましょう。コードを見て、エラーがどこにあるか教えてください:



#include <vector>
#include <algorithm>
struct X {
    int a;
};
int main() {
    std::vector<X> v = { {10}, {9}, {11} };
    //  
    std::sort(v.begin(), v.end());
}
      
      





X



1つのフィールドint



構造定義し、 その構造のオブジェクトでベクトルを埋めて、それを並べ替えようとしています。



例を読んでバグを見つけていただければ幸いです。私は答えを発表します:コンパイラはエラーが...標準ライブラリにあると考えます。診断出力は約60行の長さで、xutilityヘルパーファイル内のどこかにエラーがあることを示しています。診断を読んで理解することはほとんど不可能ですが、C ++プログラマーはそれを行います-結局のところ、テンプレートを使用する必要があります。







コンパイラは、エラーが標準ライブラリにあることを示していますが、これは、標準化委員会にすぐに書き込む必要があるという意味ではありません。実際、エラーはまだプログラムにあります。コンパイラがそれを理解するのに十分賢くないというだけで、標準ライブラリに入るとエラーが発生します。この診断を解明すると、エラーが発生します。しかしこれは:



  • 複雑な、
  • 原則として常に可能とは限りません。


C ++でのジェネリックプログラミングの最初の問題を定式化しましょう。 テンプレートを使用するときのエラーは完全に読み取れず、作成された場所ではなく、テンプレートで診断されます。



引数の型のプロパティに応じて関数の異なる実装を使用する必要がある場合は、別の問題が発生します。たとえば、2つの数値が互いに十分に近いことを確認する関数を作成したいと思います。整数の場合は、数値が等しいことを確認するだけで十分です。浮動小数点数の場合は、差がいくつかのε未満であることを確認するだけで十分です。



この問題は、2つの関数を作成することでSFINAEハックで解決できます 。ハックの用途 std::enable_if



..。これは、条件が満たされない場合にエラーを含む標準ライブラリの特別なテンプレートです。テンプレートをインスタンス化すると、コンパイラは次のエラーで宣言を破棄します。



#include <type_traits>

template <class T>
T Abs(T x) {
    return x >= 0 ? x : -x;
}

//      
template<class T>
std::enable_if_t<std::is_floating_point_v<T>, bool>
AreClose(T a, T b) {
    return Abs(a - b) < static_cast<T>(0.000001);
}

//    
template<class T>
std::enable_if_t<!std::is_floating_point_v<T>, bool> 
AreClose(T a, T b) {
    return a == b;
}
      
      





C ++ 17では、このようなプログラムはif constexpr



を使用して簡略化できますが 、すべての場合に機能するとは限りません。



または別の例:Print



何でも出力する関数を書きたい コンテナが渡された場合はすべての要素が出力され、コンテナが渡されなかった場合は渡されたものが出力されます。私はすべてのコンテナのためにそれを定義する必要があります: vector



list



set



などがあります。これは不便であり、普遍的ではありません。



template<class T>
void Print(std::ostream& out, const std::vector<T>& v) {
    for (const auto& elem : v) {
        out << elem << std::endl;
    }
}

//      map, set, list, 
// deque, array…

template<class T>
void Print(std::ostream& out, const T& v) {
    out << v;
}
      
      





SFINAEはもうここでは役に立ちません。むしろ、試してみると役に立ちますが、たくさん試してみる必要があり、コードは巨大なものになります。



ジェネリックプログラミングの2番目の問題は、異なるカテゴリの型に対して同じテンプレート関数の異なる実装を作成 する ことが難しいことです。



言語に機能を1つだけ追加して、テンプレートパラメータに制限課すと、両方の問題を簡単に解決でき ますたとえば、テンプレート化されたパラメーターが、比較をサポートするコンテナーまたはオブジェクトである必要があります。これがコンセプトです。



他の人が持っているもの



他の言語で物事がどのようになっているのか見てみましょう。私が知っている唯一の似たようなものはHaskellです。



class Eq a where
	(==) :: a -> a -> Bool
	(/=) :: a -> a -> Bool
      
      





これは、を発行する「等しい」および「等しくない」演算子のサポートを必要とする型クラスの例です Bool



C ++では、同じことが次のように行われます。



template<typename T>
concept Eq =
    requires(T a, T b) {
        { a == b } -> std::convertible_to<bool>;
        { a != b } -> std::convertible_to<bool>;
    };
      
      





まだ概念に精通していないと、何が書かれているのか理解するのが難しいでしょう。今からすべてを説明します。



Haskellでは、これらの制限が必要です。操作があると言わないと ==



使えません。C ++では、制限は厳密ではありません。コンセプトで操作を指定しなくても使用できます。結局のところ、以前はまったく制限がなく、新しい標準は以前の標準との互換性を侵害しないように努めています。





最近エラーを探していたプログラムのコードを補足しましょう。



#include <vector>
#include <algorithm>
#include <concepts>

template<class T>
concept IterToComparable = 
    requires(T a, T b) {
        {*a < *b} -> std::convertible_to<bool>;
    };
    
//    IterToComparable   class
template<IterToComparable InputIt>
void SortDefaultComparator(InputIt begin, InputIt end) {
    std::sort(begin, end);
}

struct X {
    int a;
};

int main() {
    std::vector<X> v = { {10}, {9}, {11} };
    SortDefaultComparator(v.begin(), v.end());
}
      
      





ここでコンセプトを作成しました IterToComparable



。タイプT



がイテレータであることを示しており、 比較できる値を示しています。比較の結果は、bool



たとえば、それ自体に変換可能なものになり ます bool



。詳細な説明は少し後で提供されます。今のところ、このコードを詳しく調べる必要はありません。



ちなみに、制限は弱いです。型がイテレータのすべてのプロパティを満たさなければならないというわけではありません。たとえば、インクリメントする必要はありません。これは、可能性を示す簡単な例です。



この概念は、単語の代わりに、class



または typename



cの構成で使用され ました template



。以前はそう template<class InputIt>



でしたが、今では class



コンセプトの名前に置き換えられました。したがって、パラメータ InputIt



は制約を満たす必要があります。



さて、このプログラムをコンパイルしようとすると、エラーは標準ライブラリではなく、あるべき姿でポップアップし main



ます。また、エラーには必要な情報がすべて含まれているため、エラーは理解できます。



  • 何が起こった?制約が満たされていない関数呼び出し。
  • どの制約が満たされていませんか? IterToComparable<InputIt>



  • どうして?式が((* a) < (* b))



    無効です。




コンパイラの出力は読み取り可能で、60行ではなく16行かかります。



main.cpp: In function 'int main()':
main.cpp:24:45: error: **use of function** 'void SortDefaultComparator(InputIt, InputIt) [with InputIt = __gnu_cxx::__normal_iterator<X*, std::vector<X> >]' **with unsatisfied constraints**
   24 |     SortDefaultComparator(v.begin(), v.end());
      |                                             ^
main.cpp:12:6: note: declared here
   12 | void SortDefaultComparator(InputIt begin, InputIt end) {
      |      ^~~~~~~~~~~~~~~~~~~~~
main.cpp:12:6: note: constraints not satisfied
main.cpp: In instantiation of 'void SortDefaultComparator(InputIt, InputIt) [with InputIt = __gnu_cxx::__normal_iterator<X*, std::vector<X> >]':
main.cpp:24:45:   required from here
main.cpp:6:9:   **required for the satisfaction of 'IterToComparable<InputIt>'** [with InputIt = __gnu_cxx::__normal_iterator<X*, std::vector<X, std::allocator<X> > >]
main.cpp:7:5:   in requirements with 'T a', 'T b' [with T = __gnu_cxx::__normal_iterator<X*, std::vector<X, std::allocator<X> > >]
main.cpp:8:13: note: the required **expression '((* a) < (* b))' is invalid**, because
    8 |         {*a < *b} -> std::convertible_to<bool>;
      |          ~~~^~~~
main.cpp:8:13: error: no match for 'operator<' (operand types are 'X' and 'X')
      
      





欠落している比較操作を構造に追加すると、プログラムはエラーなしでコンパイルされます-概念は満たされています:



struct X {
    auto operator<=>(const X&) const = default;
    int a;
};
      
      





同様に、2番目の例pを改善できます enable_if



このテンプレートは不要になりました。代わりに標準の概念を使用し is_floating_point_v<T>



ます。2つの関数を取得します。1つは浮動小数点数用で、もう1つは他のオブジェクト用です。



#include <type_traits>

template <class T>
T Abs(T x) {
    return x >= 0 ? x : -x;
}

//      
template<class T>
requires(std::is_floating_point_v<T>)
bool AreClose(T a, T b) {
    return Abs(a - b) < static_cast<T>(0.000001);
}

//    
template<class T>
bool AreClose(T a, T b) {
    return a == b;
}
      
      





印刷機能も変更します。呼び出しa.begin()



a.end()



言う場合、 そのa



コンテナを想定し ます。



#include <iostream>
#include <vector>

template<class T>
concept HasBeginEnd = 
    requires(T a) {
        a.begin();
        a.end();
    };

template<HasBeginEnd T>
void Print(std::ostream& out, const T& v) {
    for (const auto& elem : v) {
        out << elem << std::endl;
    }
}

template<class T>
void Print(std::ostream& out, const T& v) {
    out << v;
}
      
      





コンテナが持つだけで何かないので繰り返しますが、これは、理想的な例ではない begin



end



、それに課せられたより多くの要件があります。しかし、すでに悪くはありません。 前の例の



ようis_floating_point_v



に、既成の概念を使用するのが最善 です。コンテナの類似物の場合、標準ライブラリにも概念があります- std::ranges::input_range



しかし、それはまったく別の話です。



理論



コンセプトが何であるかを理解する時が来ました。ここでは実際には複雑なことは何もありません。



概念は制約の名前です。



私たちはそれを別の概念に還元しました。その定義はすでに意味がありますが、奇妙に思えるかもしれません。



制約は定型的な表現です。



大まかに言えば、上記の条件は「イテレータである」または「浮動小数点数である」-これらは制限です。イノベーションの本質はまさに限界にあり、その概念はそれらを参照する方法にすぎません。



最も単純な制限はこれ true



です。どんなタイプでも彼に似合う。



template<class T> concept C1 = true;
      
      





ブール演算および他の制約の組み合わせは、制約に使用できます。



template <class T>
concept Integral = std::is_integral<T>::value;

template <class T>
concept SignedIntegral = Integral<T> &&
                         std::is_signed<T>::value;
template <class T>
concept UnsignedIntegral = Integral<T> &&
                           !SignedIntegral<T>;
      
      





制約で式を使用したり、関数を呼び出したりすることもできます。ただし、関数はconstexprである必要があります。コンパイル時に計算されます。



template<typename T>
constexpr bool get_value() { return T::value; }
 
template<typename T>
    requires (sizeof(T) > 1 && get_value<T>())
void f(T); // #1
 
void f(int); // #2
 
void g() {
    f('A'); //  #2.
}
      
      





そして、可能性のリストはそれだけではありません。



制約には優れた機能があります。式の正しさをチェックすることです。エラーなしでコンパイルされます。制限を見てください Addable



角かっこで囲まれています a + b



a



b



タイプ T



がそのようなレコードを許可する場合、つまり T



、特定の加算操作がある場合、制約条件が満たされます



template<class T>
concept Addable =
requires (T a, T b) {
    a + b;
};
      
      





より複雑な例は、関数swap



とを 呼び出すこと forward



です。このコードがエラーなしでコンパイルされると、制約が実行されます。



template<class T, class U = T>
concept Swappable = requires(T&& t, U&& u) {
    swap(std::forward<T>(t), std::forward<U>(u));
    swap(std::forward<U>(u), std::forward<T>(t));
};
      
      





別のタイプの制約はタイプ検証です。



template<class T> using Ref = T&;
template<class T> concept C =
requires {
    typename T::inner; 
    typename S<T>;     
    typename Ref<T>;   
};
      
      





制約は、式の正確さだけでなく、その値のタイプが何かに対応していることも必要とする場合があります。ここに私達は書く:



  • 中括弧での表現、
  • ->,



  • 別の制限。


template<class T> concept C1 =
requires(T x) {
    {x + 1} -> std::same_as<int>;
};
      
      





この場合の制限- same_as<int>





つまり、式の型は x + 1



正確にである必要があります int







矢印の後には、タイプ自体ではなく、制約が続くことに注意してください。概念の別の例を確認してください。



template<class T> concept C2 =
requires(T x) {
    {*x} -> std::convertible_to<typename T::inner>;
    {x * 1} -> std::convertible_to<T>;
};
      
      





2つの制限があります。1つ目は、次のことを示しています。



  • 式は*x



    正しいです。
  • タイプはT::inner



    正しいです。
  • タイプはに*x



    変換されますT::inner.





1行に3つの要件があります。2番目は次のことを示しています。



  • 式はx * 1



    構文的に正しいです。
  • その結果はに変換されT



    ます。


上記の方法を使用して、任意の制限を形成できます。とても楽しくて楽しいですが、すぐに十分に手に入れて、使えなかったら忘れてしまいます。また、テンプレートをサポートするものすべてに制約と概念を使用できます。もちろん、主な用途は関数とクラスです。



だから、私たちは制約を書く方法を理解しました 、今私はあなたがそれらを書くことができる場所をあなたに教え ます



関数制約は、次の3つの異なる場所に記述できます。



//   class  typename   .
//   .
template<Incrementable T>
void f(T arg);

//    requires.       
//     .
//    .
template<class T>
requires Incrementable<T>
void f(T arg);

template<class T>
void f(T arg) requires Incrementable<T>;
      
      





そして、4番目の方法があります。これは非常に魔法のように見えます。



void f(Incrementable auto arg);
      
      





ここでは暗黙のテンプレートが使用されます。 C ++ 20までは、ラムダでのみ使用可能でした。これでauto



、任意の関数シグネチャで 使用できます void f(auto arg)



。さらに、auto



例のように、この前に 概念名を使用できます。ちなみに、明示的なテンプレートはラムダで利用できるようになりましたが、それについては後で詳しく説明します。



重要な違い:を書くときはrequires



、任意の制約を書き留めることができます。それ以外の場合は、概念の名前だけを書き留める ことができます。



クラスの可能性は少なく、2つの方法しかありません。しかし、これで十分です。



template<Incrementable T>
class X {};
template<class T>
requires Incrementable<T>
class Y {};
      
      





この記事の準備を手伝ったAntonPolukhinは、この単語requires



が関数、クラス、概念を宣言するときだけでなく、関数やメソッドの本体でも使用できることに気づき ました。たとえば、これまで未知のタイプのコンテナを埋める関数を作成する場合に便利です。



template<class T> 
void ReadAndFill(T& container, int size) { 
    if constexpr (requires {container.reserve(size); }) { 
        container.reserve(size); 
    }

    //   
}
      
      





この関数は、との両方 同じように機能します。 最初に、その場合に必要なメソッドが呼び出され ます。 に便利 です 。このようにして、通常の条件の充足だけでなく、任意のコードの正確さ、型内のメソッドと操作の存在を確認できます。 興味深いことに、コンセプトには複数のテンプレートパラメータを含めることができます。概念を使用するときは、1つを除くすべてを指定する必要があります-制約をチェックしているものです。 vector



list



reserve







requires



static_assert











template<class T, class U>
concept Derived = std::is_base_of<U, T>::value;
 
template<Derived<Other> X>
void f(X arg);
      
      





コンセプトには Derived



2つのテンプレートパラメータがあります。宣言で f



は、そのうちの1つと、X



チェックされるクラスを示しました 聴衆は私が示したパラメータを尋ねられました: T



または U



; それは機能しました Derived<Other, X>



Derived<X, Other>







答えは明らかではありません:それは Derived<X, Other>



です。パラメータOther



を指定するときに 、2番目のテンプレートパラメータを指定しました。投票結果は発散しました:



  • 正解-8(61.54%);
  • 間違った答え-5(38.46%)。


コンセプトのパラメータを指定するときは、最初のものを除くすべてを指定する必要があり、最初のものがチェックされます。なぜ委員会がそのような決定を下したのか、長い間考えていましたが、あなたもそう考えることをお勧めします。コメントにあなたのアイデアを書いてください。



そこで、新しい概念を定義する方法を説明しましたが、これは必ずしも必要ではありません。標準ライブラリにはすでにたくさんの概念があります。このスライドは、<concepts>ヘッダーファイルにある概念を示しています。







それだけではありません。<iterator>、<ranges>、およびその他のライブラリでさまざまなタイプのイテレータをテストするための概念があります。







状態







「概念」はいたるところにありますが、VisualStudioにはまだ完全にはありません。



  • GCC。バージョン10以降は十分にサポートされています。
  • Clang。バージョン10での完全なサポート。
  • VisualStudio。VS 2019でサポートされていますが、完全には実装されていない必要があります。


結論



放送中に、私たちは聴衆にこの機能が好きかどうか尋ねました。調査結果:



  • スーパー機能-50(92.59%)
  • つまり、機能-0(0.00%)
  • 不明-4(7.41%)


投票した人の圧倒的多数は、概念を高く評価しました。これもクールな機能だと思います。委員会に感謝します!



Habrの読者とウェビナーリスナーには、イノベーションを評価する機会が与えられます。



All Articles