C++20コンセプト(concepts)入門

11507 ワード

文書ディレクトリ
  • C++20コンセプト入門
  • 導入
  • カスタム制限:概念をテンプレートクラスの制限として
  • を使用する.
  • 概念をブール値として使用する
  • 拘束式(require expression)
  • 拘束式の具体的な要件
  • 概念を表す他の方法
  • 拘束従属文(require clause)
  • 概念自動変数
  • C++20コンセプト(concepts)入門
    参考資料:cppreference(より多くのライブラリ関数が提供する概念はここで参照できます)
    参考資料:c++20 concept
    参考資料:C++20 Concept文法
    参考資料:concept for C++20用法紹介
    この文書で使用するコンパイラはgcc 10.1のg++であり、コンパイルオプション-std=c++20を加えている.
    本文は長いように見えますが、主にコードが少し幅を占めているからです.C++が大丈夫であることを望みます
    導入
    まず、C++17に導入されたgcd関数(numericヘッダファイル)を見てみましょう.
    //    
    template
      constexpr common_type_t<_mn _nn="">
      gcd(_Mn __m, _Nn __n)
      {
        static_assert(is_integral_v<_mn>, "gcd arguments are integers");
        static_assert(is_integral_v<_nn>, "gcd arguments are integers");
        static_assert(!is_same_v, bool>,
        "gcd arguments are not bools");
        static_assert(!is_same_v, bool>,
        "gcd arguments are not bools");
        return __detail::__gcd(__m, __n);
      }
    

    このgcd関数はパッケージにすぎず、実際に計算される関数は__gcdであり、ここでは示されていない.このパッケージ関数は、__m__nが整数タイプであるかどうかを確認するために追加されました.もっと簡単な表示方法はありますか?
    C++20の概念はこれを行うことができる.
    プログラム1:最初のコンセプトプログラム
    #include 
    #include 
    template<:unsigned_integral t1="" std::unsigned_integral="" t2="">
    constexpr std::common_type_t gcd(T1 a, T2 b)
    {
    	return b ? gcd(b, a % b) : a;
    }
    int main()
    {
    	std::cout << gcd(37u, 666u) << std::endl;
    	std::cout << gcd(1, 1.0) << std::endl;
    	return 0;
    }
    
    typenamestd::unsigned_integralに置き換えられた.std::unsigned_integralconceptsに定義され、以下のように実現される.
    template 
    concept integral = is_integral_v<_ty>;
    template 
    concept signed_integral = integral<_ty> && _Ty(-1) < _Ty(0);
    template 
    concept unsigned_integral = integral<_ty> && !signed_integral<_ty>;
    

    このうちconceptは新しいキーワードで、具体的にどういう意味かはしばらくは関係ありませんが、整数タイプかどうかを判断する作業はC++11にあるtype_traitsで行われていることがわかります.概念の導入は、コードの構造をより良くするために行われただけです.このような定義により、プログラム1はmain関数の2番目の文のコードコンパイルエラーに起因すると断言できます.事実、コンパイラは次のように出力します.
    demo.cpp:11:28: error: use of function 'constexpr std::common_type_t<_tp1 _tp2=""> gcd(T1, T2) [with T1 = int; T2 = double; std::common_type_t<_tp1 _tp2=""> = double]' with unsatisfied constraints
       11 |     std::cout << gcd(1, 1.0) << std::endl;
    

    ここで、問題を簡略化するために、abはいずれも符号なし整数であることが規定されているので、実際にはこのコードの2つのパラメータは私たちの要求に合致しない.これも、最初の文コードが整数の後に接尾辞uを付ける理由である.しかしながら、gcd(0u, true)と書くと、numericgcdが要求するパラメータがブール型の精神的に一致していないことを要求するパラメータと一致しないことが発見され、どのようにして同時に制限することができるのか.(注:両方のパラメータがブール型の場合、関数内部で再帰的に呼び出されるためコンパイルできません.両方のパラメータがブール型の場合、a % bはデフォルトでint型に変換され、int型はコンパイルできません)
    カスタム制限かすたむせいげん:テンプレートクラスの制限としてコンセプトを使用します
    ライブラリ内の構文を真似して、カスタムコンセプトを作成できます.
    プログラム2:カスタムコンセプト
    #include 
    #include 
    template
    concept gcdint = std::unsigned_integral && !std::is_same_v<:remove_cv_t>, bool>;
    
    template
    constexpr std::common_type_t gcd(T1 a, T2 b)
    {
    	return b ? gcd(b, a % b) : a;
    }
    int main()
    {
    	std::cout << gcd(37u, 666u) << std::endl;
    	std::cout << gcd(0u, false) << std::endl;
    	return 0;
    }
    
    gcd(0u, false)はコンパイルできません.コンパイラが間違っています.
    demo.cpp:13:28: error: use of function 'constexpr std::common_type_t<_tp1 _tp2=""> gcd(T1, T2) [with T1 = unsigned int; T2 = bool; std::common_type_t<_tp1 _tp2=""> = unsigned int]' with unsatisfied constraints
       13 |  std::cout << gcd(0u, false) << std::endl;
    

    一説によれば、このように書くのは作者の意味に対して確かにもっと分かりやすい.注意が必要なのは、このようなコードはC++11では完全に等価な書き方がありますが、これよりも長く、こんなに分かりやすいものではありません.
    概念をブール値として使用
    概念は以上の用法に加えて,コンパイル時に決定されたブール値として直接使用することもできる.これは、前のコードのstd::is_same_vと同様の性質を有する.次に、C++20が新たに導入したstd::same_asの概念を例に、テンプレート内定数std::is_same_vの代替とする.
    プログラム3:是非
    #include 
    #include 
    #include 
    int main()
    {
    	std::cout << "(in C++11) int is the same as long: " << std::is_same_v << std::endl;
    	std::cout << "int is the same as long: " << std::same_as << std::endl;
    	std::cout << "int is the same as __int32: " << std::same_as << std::endl;
    	std::cout << "long is the same as __int32: " << std::same_as << std::endl;
    	return 0;
    }
    

    実行結果:
    (in C++11) int is the same as long: 0
    int is the same as long: 0
    int is the same as __int32: 1
    long is the same as __int32: 0
    
    same_asという概念の可能な内部実装がis_same_vであるため、結果が変わる心配はありません.
    //    
    namespace __detail
    {
      template
        concept __same_as = std::is_same_v<_tp _up="">;
    } // namespace __detail
    

    制約式(require expression)
    コンセプトライブラリには、コンストレイントエクスプレッション(require expression)という重要なものも導入されています.コンストレイントエクスプレッションはLambdaエクスプレッションに似ていますが、用途や効果はまったく異なります.Lambdaエクスプレッションの結果はLambdaオブジェクトであり、コンストレイントエクスプレッションの結果はコンセプトです.
    コンストレイント式の例は次のとおりです.
    プログラム4:アヒルのタイプ
    #include 
    #include 
    #include 
    template
    concept duck_type = requires(T x)
    {
    	x.quack();
    	x.quack("quack");
    };
    
    class T1 {};
    class T2
    {
    public:
    	void quack() const {}
    	void quack(const std::string& b) const { std::cout << b << std::endl; }
    };
    class T3 : public T1
    {
    public:
    	int quack(const std::string& b = "quack") const { std::cout << b << std::endl; return 0; }
    };
    
    template
    void quack(const T& x)
    {
    	x.quack();
    }
    
    int main()
    {
    	// quack(T1()); // error: use of function 'void quack(const T&) [with T = T1]' with unsatisfied constraints
    	quack(T2());
    	quack(T3());
    	return 0;
    }
    

    テンプレートの定義から、typenameを直接使用しても間違いないようで、より具体的な結果が得られます:error: 'const class T1' has no member named 'quack'.しかし、コンストレイントが多い場合は、コンストレイント式を使用すると、どのコンストレイントが悪いのかを具体的に説明できますが、typenameを直接使用すると、コンパイルエラーが大きくなる可能性があります.このような直感はありません.
    制約式の具体的な要件
    実際,制約式の内部は直接関数のように書けばよいのではなく,一定の規範に従わなければならない.コンストレイント式の括弧の内部は、関数体ではなくコンストレイントリストと呼ばれます.
    コンストレイントは、単純コンストレイント、タイプコンストレイント、複雑コンストレイント、ネストコンストレイントの4つに大きく分けることができます.
    単純制約の形式は、非制約式の式(概念であってもよい)であり、この式がコンパイルされたり、この概念が真であったりすることができる場合、この制約式が返す概念のブール値は真であり、そうでなければ偽である可能性があります.たとえば、前例のx.quack()x.quack("quack")は、2つの単純制約です.
    タイプコンストレイントの形式はtypename xxx;です.xxxがタイプである場合、この制約式が返す概念のブール値は真であり、そうでなければ偽である可能性があります.
    プログラム5:反復可能オブジェクトチェック
    #include 
    #include 
    #include 
    template
    concept weak_iterable = requires(T x)
    {
    	typename T::iterator;
    	x.begin();
    	x.end();
    };
    
    int main()
    {
    	std::cout << "is std::string iterable: " << weak_iterable<:string> << std::endl;
    	return 0;
    }
    

    実行結果:
    is std::string iterable: 1
    

    複雑な制約とは、より複雑な形式の制約を指します.たとえば、式に異常を投げ出せないようにするには、次の構文を使用します.
    {xxx} noexcept;
    

    このうちxxxには末尾のセミコロンは含まれていません.
    式の値がタイプ(暗黙的にタイプに変換可能)であることを要求する場合は、次の例を参照してください.
    プログラム6:式のタイプを確認する
    #include 
    #include 
    #include 
    template
    concept newable = requires(T x)
    {
    	{new T} -> std::same_as;
    };
    
    int main(int argn, char** argv)
    {
    	std::cout << "is int newable: " << newable << std::endl;
    	std::cout << requires() { { main(0, nullptr) } -> std::same_as; } << std::endl;
    	std::cout << requires { { main(0, nullptr) } -> std::convertible_to; } << std::endl;
    	return 0;
    }
    

    実行結果:
    is int newable: 1
    1
    1
    
    {xxx} -> yyyの役割は、xxxのタイプをテンプレートパラメータとしてyyyテンプレートに伝達する最後のパラメータであり、yyyは概念である必要があると推測される.コンパイラによると、yyytype-specifierであるべきだ.
    最後にネストされたコンストレイントです.ネストされた制約は、制約リストにrequires(xxx);の形式の制約式が存在することを意味し、この形式の制約式は、xxxが静的断言を通過することを要求することを示す.なお、この形式のコンストレイント式はコンストレイント式にのみ表示されます.
    プログラム7:ネストコンストレイント
    #include 
    #include 
    #include 
    template
    concept int64 = requires(T x)
    {
    	std::integral;
    	requires(sizeof(x) == 8);
    };
    
    int main(int argn, char **argv)
    {
    	std::cout << int64 << std::endl;
    	std::cout << int64 << std::endl;
    	return 0;
    }
    

    実行結果:
    1
    0
    

    コンストレイント式はテンプレートを使用しなくてもよいが、コンセプトはテンプレートを使用する必要があることに注意してください.
    概念を表す他の方法
    コンストレイント従文(require clause)
    次にrequiresキーワードをもう一度使用しますが、ここのrequiresキーワードは上記と同じではありません.つまり、requiresの意味は文脈によって異なります.
    プログラム8:拘束従文
    #include 
    #include 
    #include 
    #include 
    
    template 
    requires std::integral && requires { requires(sizeof(T) == 4); } && (sizeof(T) == 4)
    T func(T x) { return x + 1; }
    
    int main(int argn, char **argv)
    {
    	std::cout << func(1) << std::endl;
    	// std::cout << func(1.0) << std::endl;
    	return 0;
    }
    

    すなわち、上記の例のように、Tが満たす概念を事前に指定することなく、事前に指定する場合は、グローバルな概念を定義する必要があります.このように、Tが満たす必要がある概念を制約従文で表すことで、グローバル概念の定義を回避することができる.
    前述の例のように、制約従文の構文構造はrequires xxxである.xxxは、基本式(primary expression)または論理演算子によって接続された複数の基本式です.簡単に言えば、不明なコンパイルエラーが発生した場合は、カッコを付けて、非基本式をベース式にすることができます((xxx)は常に基本式であるため).
    概念自動変数
    プログラム⑨:1+1=?
    #include 
    #include 
    #include 
    #include 
    
    std::integral auto add(std::integral auto a, std::same_as auto b)
    {
    	return a + b;
    }
    
    int main(int argn, char **argv)
    {
    	std::cout << add(1ll, 1) << std::endl;
    	return 0;
    }
    

    この書き方はもっと簡単に見えますが、その中の行為を理解しにくいように見えます.でも、みんなが読めると信じています!
    間違いを見つけたら、この文章の間違いを指摘してほしい.ありがとうございます.