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++であり、コンパイルオプション
本文は長いように見えますが、主にコードが少し幅を占めているからです.C++が大丈夫であることを望みます
導入
まず、C++17に導入された
この
C++20の概念はこれを行うことができる.
プログラム1:最初のコンセプトプログラム
このうち
ここで、問題を簡略化するために、
カスタム制限かすたむせいげん:テンプレートクラスの制限としてコンセプトを使用します
ライブラリ内の構文を真似して、カスタムコンセプトを作成できます.
プログラム2:カスタムコンセプト
一説によれば、このように書くのは作者の意味に対して確かにもっと分かりやすい.注意が必要なのは、このようなコードはC++11では完全に等価な書き方がありますが、これよりも長く、こんなに分かりやすいものではありません.
概念をブール値として使用
概念は以上の用法に加えて,コンパイル時に決定されたブール値として直接使用することもできる.これは、前のコードの
プログラム3:是非
実行結果:
制約式(require expression)
コンセプトライブラリには、コンストレイントエクスプレッション(require expression)という重要なものも導入されています.コンストレイントエクスプレッションはLambdaエクスプレッションに似ていますが、用途や効果はまったく異なります.Lambdaエクスプレッションの結果はLambdaオブジェクトであり、コンストレイントエクスプレッションの結果はコンセプトです.
コンストレイント式の例は次のとおりです.
プログラム4:アヒルのタイプ
テンプレートの定義から、typenameを直接使用しても間違いないようで、より具体的な結果が得られます:
制約式の具体的な要件
実際,制約式の内部は直接関数のように書けばよいのではなく,一定の規範に従わなければならない.コンストレイント式の括弧の内部は、関数体ではなくコンストレイントリストと呼ばれます.
コンストレイントは、単純コンストレイント、タイプコンストレイント、複雑コンストレイント、ネストコンストレイントの4つに大きく分けることができます.
単純制約の形式は、非制約式の式(概念であってもよい)であり、この式がコンパイルされたり、この概念が真であったりすることができる場合、この制約式が返す概念のブール値は真であり、そうでなければ偽である可能性があります.たとえば、前例の
タイプコンストレイントの形式は
プログラム5:反復可能オブジェクトチェック
実行結果:
複雑な制約とは、より複雑な形式の制約を指します.たとえば、式に異常を投げ出せないようにするには、次の構文を使用します.
このうち
式の値がタイプ(暗黙的にタイプに変換可能)であることを要求する場合は、次の例を参照してください.
プログラム6:式のタイプを確認する
実行結果:
最後にネストされたコンストレイントです.ネストされた制約は、制約リストに
プログラム7:ネストコンストレイント
実行結果:
コンストレイント式はテンプレートを使用しなくてもよいが、コンセプトはテンプレートを使用する必要があることに注意してください.
概念を表す他の方法
コンストレイント従文(require clause)
次に
プログラム8:拘束従文
すなわち、上記の例のように、
前述の例のように、制約従文の構文構造は
概念自動変数
プログラム⑨:1+1=?
この書き方はもっと簡単に見えますが、その中の行為を理解しにくいように見えます.でも、みんなが読めると信じています!
間違いを見つけたら、この文章の間違いを指摘してほしい.ありがとうございます.
参考資料: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;
}
typename
はstd::unsigned_integral
に置き換えられた.std::unsigned_integral
はconcepts
に定義され、以下のように実現される.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;
ここで、問題を簡略化するために、
a
とb
はいずれも符号なし整数であることが規定されているので、実際にはこのコードの2つのパラメータは私たちの要求に合致しない.これも、最初の文コードが整数の後に接尾辞u
を付ける理由である.しかしながら、gcd(0u, true)
と書くと、numeric
のgcd
が要求するパラメータがブール型の精神的に一致していないことを要求するパラメータと一致しないことが発見され、どのようにして同時に制限することができるのか.(注:両方のパラメータがブール型の場合、関数内部で再帰的に呼び出されるためコンパイルできません.両方のパラメータがブール型の場合、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
は概念である必要があると推測される.コンパイラによると、yyy
はtype-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;
}
この書き方はもっと簡単に見えますが、その中の行為を理解しにくいように見えます.でも、みんなが読めると信じています!
間違いを見つけたら、この文章の間違いを指摘してほしい.ありがとうございます.