C++20 のコンセプトと制約

最終更新: 2025-12-23 21:51:39 +0800 +0800
原文は英語で書かれています。あなたはAI翻訳版を読んでいます。

C++20 のコンセプトにより、テンプレート宣言のインターフェイスでコンパイラーが検証 できる要件を記述できます。制約付き候補は、関連する制約が満たされた場合にだけ オーバーロード解決へ参加します。そのため制約は、多くの std::enable_if SFINAE の 用途を、意図した操作集合を公開する宣言に置き換えます。ただし API が文書化した 意味上の要件を証明するものではありません。

制約を宣言する #

コンセプトは名前空間スコープの変数のようなテンプレートであり、定義は bool の 定数式です。

cpp
#include <concepts>

template <class T>
concept Addable = requires(T value) {
    { value + value } -> std::convertible_to<T>;
};

コンセプトは requires 節、制約付きテンプレートパラメーター、短縮関数テンプレートで 適用できます。

cpp
template <class T>
    requires Addable<T>
T sum(T left, T right) {
    return left + right;
}

std::integral auto increment(std::integral auto value) {
    return value + 1;
}

各 std::integral auto は別個のテンプレートパラメーターを導入します。2 つの引数が 同じ推論型でなければならない場合は明示的なパラメーターを使います。宣言子の後に 制約を置く場合、末尾の requires 節が便利です。クラステンプレートのメンバーにも 使用できます。

cpp
template <class T>
struct Buffer {
    void compact() requires std::movable<T>;
};

requires 節はテンプレートでない自由関数には記述できません。複数の宣言位置の制約は && で結合されます。置換が前提条件に依存する要件より前に、その前提条件を置きます。

requires 式と意味上の要件 #

requires 式はオペランドを評価せずに要件を確認します。単純要件、型要件、複合要件、 ネストした要件をサポートします。

cpp
#include <concepts>
#include <iterator>

template <class T>
concept SortableRange = requires(T range) {
    typename T::value_type;                         // type
    { range.begin() } -> std::input_or_output_iterator; // compound
    range.end();                                    // simple
    requires std::movable<typename T::value_type>;  // nested
};

置換後に無効な要件があると、呼び出し箇所で診断を出すのではなく、依存する制約が 満たされない状態になります。あらゆる置換で無効な要件は、診断不要の不適格形式です。

充足は構文上のものです。型は等価性の式要件を満たしながら、std::equality_comparable が期待する同値関係に違反することがあります。swap をコンパイル可能にしながら、 指定された計算量や事後条件に違反する型もあります。コンパイラーはこれらを確立 できないため、意味上の要件を文書化してテストします。

enable_if から移行する #

コンセプトが利用できるという理由だけでオーバーロード集合を広げず、既存の選択 ポリシーを移行します。

cpp
// Before C++20.
template <class T, std::enable_if_t<std::integral<T>, int> = 0>
T twice(T value) { return value + value; }

// C++20, with the same intended eligibility.
template <std::integral T>
T twice(T value) { return value + value; }

制約は候補の有効性を選択しますが、選択された関数本体のエラーを置換失敗には変えません。 enable_if を置き換える際は、除外ケース、変換、順序付けテストを保持します。特に 古いオーバーロードがデフォルトテンプレートパラメーターや戻り値型 SFINAE を使って いた場合は注意します。

順序付けと包含 #

複数の制約付きオーバーロードが満たされる場合、言語は制約の包含を使って、より制約 された候補を選びます。包含は構造的なもので、任意のブール同値ではなく正規化された 原子制約に基づきます。共有する述語に名前を付けて組み合わせます。

cpp
template <class T>
concept Range = requires(T value) { value.begin(); value.end(); };

template <class T>
concept SizedRange = Range<T> && requires(T value) { value.size(); };

template <Range T>
void inspect(const T&);

template <SizedRange T>
void inspect(const T&); // selected when both are satisfied

たまたま同じ意味になる式を繰り返すと、異なる原子制約が作成され、オーバーロードが 曖昧になる可能性があります。名前付きコンセプトは、順序付け規則に必要な原子制約の 同一性を保持します。

転送参照とカスタマイズポイント #

T が推論される T&& では、lvalue 引数に対して T は lvalue 参照型として推論 されます。契約が基底オブジェクトに関係する場合は、std::remove_cvref_t<T> に制約 を付けます。

cpp
#include <ranges>
#include <type_traits>

template <class R>
    requires std::ranges::range<std::remove_cvref_t<R>>
void consume(R&& range);

参照修飾が意図されている場合にだけ R に直接制約を付けます。この区別により、 オブジェクト型向けに書かれたコンセプトだけを理由に lvalue が拒否されることを防げます。

カスタマイズポイントオブジェクト(CPO)は通常、制約付き呼び出し演算子が定義された カスタマイズプロトコルを実装する inline constexpr 関数オブジェクトです。たとえば std::ranges::begin は、配列、適切なメンバー begin、または引数依存ルックアップで 見つかる非修飾 begin からイテレーターを取得できます。CPO 境界でサポートする プロトコルを制約に記述し、サポートされない型が選択されたカスタマイズの深部ではなく、 そのインターフェイスで失敗するようにします。

コメント

GitHub で閲覧・コメントする(投稿にはログインが必要です)