C++ 基礎からマスターまで — 第16回
Concepts と requires でテンプレート要件を表現する
本ブログの記事は、講義を目的として作成しています。 内容に誤りが含まれる可能性があるため、対象コンパイラと標準ライブラリの資料も確認してください。
テンプレートは同じ規則を複数の型に適用できますが、未対応の型を渡すと実装の奥深くで長いエラーが発生しがちです。 C++20 の Concepts と requires は、テンプレートが要求する操作を公開宣言に表現します。
制約のないテンプレート
template <typename T>
int GetHealth(const T& value)
{
return value.Health();
}
この関数は Health() を呼べる型だけに使えますが、シグネチャからは条件が分かりません。コンパイラは関数本体をインスタンス化して初めて問題を検出します。
Concept で要件に名前を付ける
#include <concepts>
template <typename T>
concept Damageable = requires(T value, int amount)
{
{ value.Health() } -> std::convertible_to<int>;
{ value.ApplyDamage(amount) } -> std::same_as<void>;
};
template <Damageable T>
void Attack(T& target, int damage)
{
target.ApplyDamage(damage);
}
requires expression はコードを実行せず、構文と結果型が有効かを検査します。Attack の契約が本体より前に見え、不正な呼び出しの診断も呼び出し位置に近づきます。
型の性質も組み合わせる
template <typename T>
concept SmallId = std::integral<T> && (sizeof(T) <= 4);
template <SmallId T>
void WriteId(T id);
Concept は type trait と requires を論理演算で組み合わせられ、制約はオーバーロードの優先順位にも関わります。
void Print(std::integral auto value);
void Print(std::floating_point auto value);
Concept を付けた auto は関数テンプレートの短縮記法です。整数と浮動小数点で意味が異なるなら、大きな if constexpr より個別のオーバーロードが明確です。
構文が有効でも意味は保証されない
a < b がコンパイルできても、一貫した順序を作るとは限りません。コピー可能という構文だけでは、コピー後の値が意味的に独立していることも保証できません。
標準 Concept は構文に加えて意味的要件も文書化します。独自 Concept には意図を表す名前を付け、重要な意味はテストで検証します。
必要以上に型を制限しないことも大切です。実装と公開 API が本当に必要とする最小の契約を表現してください。