concepts(C++20)
『C++20』で導入された concept(コンセプト)は、テンプレート引数に対する制約を型安全かつ読みやすい形で記述する仕組みです。コンセプトを使うと、「この型は加算できること」「このクラスは特定のメンバ関数を持つこと」といった要件をコンパイル時に検査でき、要件を満たさない型が渡された場合はわかりやすいエラーメッセージが得られます。
構文
// ========================================
// concept(コンセプト)の基本構文
// ========================================
#include <concepts>
// concept キーワードでコンセプトを定義します
// requires 節に制約式を書きます(bool 定数式)
template <typename T>
concept MyConcept = requires(T value) {
// T が加算演算子を持つことを要求します
{ value + value } -> std::convertible_to<T>;
// T がメンバ関数 name() を持つことを要求します
{ value.name() } -> std::convertible_to<std::string>;
};
// コンセプトをテンプレート制約として使う方法 (1): requires 節
template <typename T>
requires MyConcept<T>
void process(T value) { /* ... */ }
// コンセプトをテンプレート制約として使う方法 (2): 短縮形(abbreviated template)
void process(MyConcept auto value) { /* ... */ }
// 標準ライブラリのコンセプト例
template <std::integral T> // T は整数型でなければなりません
void printInt(T n) { /* ... */ }
template <std::floating_point T> // T は浮動小数点型でなければなりません
void printFloat(T n) { /* ... */ }
// requires 式で複数の制約をまとめて記述できます
template <typename T>
concept Printable = requires(T v) {
{ std::cout << v }; // ストリーム出力ができる型であることを要求します
};
構文一覧
| 構文・概念 | 概要 |
|---|---|
| concept Name = ...; | コンセプトを定義します。右辺は bool 定数式で、テンプレート引数が満たすべき制約を記述します。 |
| requires T | テンプレートパラメータに制約を付ける requires 節です。コンセプトや論理演算(&& / ||)で複数の制約を組み合わせられます。 |
| requires(T v) { ... } | requires 式です。{ expr } -> concept; の形で、式の型や有効性をコンパイル時に検査します。 |
| ConceptName auto | abbreviated テンプレート構文です。関数引数の型として直接コンセプトを書け、テンプレート宣言を省略できます。 |
| std::integral / std::floating_point | 標準ライブラリ(<concepts>)が提供する組み込みコンセプトです。整数型・浮動小数点型などの基本的な制約を手軽に利用できます。 |
| std::same_as / std::convertible_to | 型の同一性・変換可能性を検査する標準コンセプトです。requires 式の戻り値型制約(->)と組み合わせてよく使います。 |
サンプルコード
dragonball_concept.cpp
// ========================================
// dragonball_concept.cpp
// ドラゴンボールのキャラクターを題材に
// C++20 concept によるテンプレート制約を示すサンプルです
// ========================================
#include <iostream>
#include <string>
#include <vector>
#include <concepts>
// ========================================
// コンセプト定義
// ========================================
// Warrior コンセプト:戦士として必要なインターフェースを持つ型を制約します
// - name() : キャラクター名を返します
// - powerLevel(): 戦闘力を返します
// - attack() : 攻撃を実行します
template <typename T>
concept Warrior = requires(T fighter) {
{ fighter.name() } -> std::convertible_to<std::string>;
{ fighter.powerLevel() } -> std::convertible_to<int>;
{ fighter.attack() } -> std::same_as<void>;
};
// Transformable コンセプト:変身能力を持つ型を制約します
template <typename T>
concept Transformable = Warrior<T> && requires(T fighter) {
// transform() を呼び出せることを要求します
{ fighter.transform() } -> std::same_as<void>;
};
// ========================================
// 孫悟空クラス:Transformable コンセプトを満たします
// ========================================
class SonGoku {
std::string name_;
int powerLevel_;
bool transformed_;
public:
SonGoku()
: name_("孫悟空"), powerLevel_(9000), transformed_(false) {}
std::string name() const { return name_; }
int powerLevel() const { return transformed_ ? powerLevel_ * 50 : powerLevel_; }
void attack() {
if (transformed_) {
std::cout << name_ << " が「かめはめ波(超サイヤ人)」を放ちます! 戦闘力: " << powerLevel() << std::endl;
} else {
std::cout << name_ << " が「かめはめ波」を放ちます! 戦闘力: " << powerLevel() << std::endl;
}
}
void transform() {
transformed_ = true;
std::cout << name_ << " が超サイヤ人に変身しました! 戦闘力が 50 倍になります。" << std::endl;
}
};
// ========================================
// ベジータクラス:Transformable コンセプトを満たします
// ========================================
class Vegeta {
std::string name_;
int powerLevel_;
bool transformed_;
public:
Vegeta()
: name_("ベジータ"), powerLevel_(8500), transformed_(false) {}
std::string name() const { return name_; }
int powerLevel() const { return transformed_ ? powerLevel_ * 50 : powerLevel_; }
void attack() {
if (transformed_) {
std::cout << name_ << " が「ビッグバンアタック(超サイヤ人)」を放ちます! 戦闘力: " << powerLevel() << std::endl;
} else {
std::cout << name_ << " が「ギャリック砲」を放ちます! 戦闘力: " << powerLevel() << std::endl;
}
}
void transform() {
transformed_ = true;
std::cout << name_ << " が超サイヤ人に変身しました! 戦闘力が 50 倍になります。" << std::endl;
}
};
// ========================================
// ピッコロクラス:Warrior コンセプトは満たしますが
// transform() を持たないため Transformable は満たしません
// ========================================
class Piccolo {
std::string name_;
int powerLevel_;
public:
Piccolo()
: name_("ピッコロ"), powerLevel_(3500) {}
std::string name() const { return name_; }
int powerLevel() const { return powerLevel_; }
void attack() {
std::cout << name_ << " が「魔貫光殺砲」を放ちます! 戦闘力: " << powerLevel_ << std::endl;
}
// transform() を持たないので Transformable コンセプトを満たしません
};
// ========================================
// フリーザクラス:Warrior + Transformable を満たします
// ========================================
class Frieza {
std::string name_;
int powerLevel_;
int form_; // 変身フォーム(1〜4)
public:
Frieza()
: name_("フリーザ"), powerLevel_(530000), form_(1) {}
std::string name() const { return name_; }
int powerLevel() const { return powerLevel_ * form_; }
void attack() {
std::cout << name_ << "(第" << form_ << "形態) が「デスビーム」を放ちます! 戦闘力: " << powerLevel() << std::endl;
}
void transform() {
if (form_ < 4) {
++form_;
std::cout << name_ << " が第" << form_ << "形態に変身しました! 戦闘力: " << powerLevel() << std::endl;
} else {
std::cout << name_ << " はすでに最終形態です。" << std::endl;
}
}
};
// ========================================
// Warrior コンセプトで制約したテンプレート関数
// Warrior コンセプトを満たす型ならどれでも受け取れます
// ========================================
template <Warrior T>
void showStatus(const T& fighter) {
std::cout << " 名前: " << fighter.name()
<< " / 戦闘力: " << fighter.powerLevel() << std::endl;
}
// ========================================
// Transformable コンセプトで制約したテンプレート関数
// transform() を持つ型だけが受け取れます
// ========================================
template <Transformable T>
void powerUp(T& fighter) {
std::cout << "--- パワーアップ開始: " << fighter.name() << " ---" << std::endl;
fighter.transform();
fighter.attack();
std::cout << std::endl;
}
// ========================================
// abbreviated テンプレート構文(C++20 の短縮形)
// Warrior auto で制約を直接引数に記述できます
// ========================================
void battleCry(Warrior auto& fighter) {
std::cout << fighter.name() << " が戦いに挑みます!" << std::endl;
fighter.attack();
}
int main() {
SonGoku goku;
Vegeta vegeta;
Piccolo piccolo;
Frieza frieza;
// --- 全員のステータス表示(Warrior コンセプトで制約)---
std::cout << "=== 全戦士のステータス ===" << std::endl;
showStatus(goku);
showStatus(vegeta);
showStatus(piccolo);
showStatus(frieza);
std::cout << std::endl;
// --- abbreviated テンプレート構文で戦闘開始 ---
std::cout << "=== 戦闘開始 ===" << std::endl;
battleCry(piccolo); // ピッコロは Warrior を満たすので呼び出せます
std::cout << std::endl;
// --- Transformable を満たす型だけ powerUp() を呼び出せます ---
std::cout << "=== 超サイヤ人変身 ===" << std::endl;
powerUp(goku);
powerUp(vegeta);
// powerUp(piccolo); // コンパイルエラー:ピッコロは Transformable を満たしません
// → "piccolo does not satisfy Transformable" のような明確なエラーになります
std::cout << "=== フリーザ 変身 ===" << std::endl;
powerUp(frieza); // フリーザは Transformable を満たします
powerUp(frieza); // 第3形態
powerUp(frieza); // 最終形態(第4形態)
return 0;
}
# コンパイルします(C++20 が必要です) g++ -std=c++20 dragonball_concept.cpp -o dragonball_concept && ./dragonball_concept === 全戦士のステータス === 名前: 孫悟空 / 戦闘力: 9000 名前: ベジータ / 戦闘力: 8500 名前: ピッコロ / 戦闘力: 3500 名前: フリーザ / 戦闘力: 530000 === 戦闘開始 === ピッコロ が戦いに挑みます! ピッコロ が「魔貫光殺砲」を放ちます! 戦闘力: 3500 === 超サイヤ人変身 === --- パワーアップ開始: 孫悟空 --- 孫悟空 が超サイヤ人に変身しました! 戦闘力が 50 倍になります。 孫悟空 が「かめはめ波(超サイヤ人)」を放ちます! 戦闘力: 450000 --- パワーアップ開始: ベジータ --- ベジータ が超サイヤ人に変身しました! 戦闘力が 50 倍になります。 ベジータ が「ビッグバンアタック(超サイヤ人)」を放ちます! 戦闘力: 425000 === フリーザ 変身 === --- パワーアップ開始: フリーザ --- フリーザ が第2形態に変身しました! 戦闘力: 1060000 フリーザ(第2形態) が「デスビーム」を放ちます! 戦闘力: 1060000 --- パワーアップ開始: フリーザ --- フリーザ が第3形態に変身しました! 戦闘力: 1590000 フリーザ(第3形態) が「デスビーム」を放ちます! 戦闘力: 1590000 --- パワーアップ開始: フリーザ --- フリーザ が第4形態に変身しました! 戦闘力: 2120000 フリーザ(第4形態) が「デスビーム」を放ちます! 戦闘力: 2120000
よくあるミス: コンセプトを満たさない型を渡す
コンセプトで制約したテンプレート関数に、制約を満たさない型を渡すとコンパイルエラーになります。エラーメッセージにコンセプト名が表示されるため、原因を特定しやすくなっています。
// Warrior コンセプト: name(), powerLevel(), attack() を持つ型のみ受け付けます
template <Warrior T>
void showStatus(const T& fighter) { /* ... */ }
// Warrior コンセプトを満たさない型
struct Bystander {
std::string label;
// name(), powerLevel(), attack() を持たない
};
Bystander b{"一般市民"};
showStatus(b); // コンパイルエラー: Bystander does not satisfy Warrior
OK: コンセプトが要求するメンバ関数をすべて実装します。
// OK: Warrior コンセプトを満たすクラスを定義する
class TrunksClass {
public:
std::string name() const { return "トランクス"; }
int powerLevel() const { return 7000; }
void attack() { std::cout << "未来剣を放ちます!" << std::endl; }
};
TrunksClass trunks;
showStatus(trunks); // OK
g++ -std=c++20 ok_example.cpp -o ok_example ./ok_example 名前: トランクス / 戦闘力: 7000
よくあるミス: requires 式の戻り値型制約の書き忘れ
requires 式で式の有効性だけ検査し、戻り値の型を制約しない場合、意図しない型の関数も受け入れてしまうことがあります。
// 注意: 戻り値型制約なしの requires 式
template <typename T>
concept HasName = requires(T v) {
v.name(); // name() が呼べれば型を問わない(int を返しても通る)
};
// より厳密なコンセプト: 戻り値型も制約する
template <typename T>
concept HasStringName = requires(T v) {
{ v.name() } -> std::convertible_to<std::string>; // std::string に変換できる型に限定する
};
概要
C++20 の concept は、テンプレートプログラミングの長年の課題であった「制約の不明確さ」と「難解なエラーメッセージ」を解消するために導入されました。コンセプトを定義することで「この関数は name() と powerLevel() を持つ型だけが使えます」といった意図をコード上で明示でき、要件を満たさない型が渡されたときはコンパイラが「このコンセプトを満たしません」と明確に報告してくれます。requires 節・requires 式・abbreviated テンプレート(Concept auto)の3つの記法を状況に応じて使い分けることで、制約の記述量と可読性のバランスをとれます。また <concepts> ヘッダに用意された std::integral・std::floating_point・std::convertible_to などの標準コンセプトを活用すると、よく使う型制約を自分で定義せずに済みます。コンセプトは テンプレート や 可変引数テンプレート と組み合わせることで、型安全で再利用性の高い汎用コードを記述できます。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。