コンテンツにスキップ

C++/標準ライブラリ/concepts

出典: フリー教科書『ウィキブックス(Wikibooks)』

概要[編集]

C++20から導入された<concepts>ヘッダーは、ジェネリックプログラミングをより安全かつ表現力豊かにするためのコンセプトを提供しています。コンセプトとは、型が満たすべき条件やその型に対して適用可能な操作を定義するものです。これにより、テンプレート引数の制約を明示的に表現できるようになり、コンパイル時のエラー検出が向上し、コードの理解が容易になります。

言語関連のコンセプト[編集]

same_as[編集]

same_asコンセプトは、2つの型が同一であるかどうかを検査します。このコンセプトは、テンプレート引数の制約において、特定の型が期待される場合に使用できます。

template<typename T>
concept C = same_as<T, int>; // Tがintと同じ型である必要がある

template<same_as<int> T>
void foo(T x) { /* ... */ } // fooの引数型はintでなければならない

same_asは、is_same_vの概念版です。しかし、same_asは双方向の等価性を検査するため、is_same_vよりも厳しい制約となります。

derived_from[編集]

derived_fromコンセプトは、ある型が他の型から派生しているかどうかを検査します。このコンセプトは、継承関係を要求する場合に使用できます。

struct Base {};
struct Derived : Base {};

template<typename T>
concept C = derived_from<T, Base>; // TはBaseから派生している必要がある

void foo(C auto x) { /* ... */ } // xの型はBaseまたはBaseを継承した型でなければならない

derived_fromは、is_base_ofis_convertibleの組み合わせで定義されています。これにより、安全な基底クラスへの暗黙の変換が保証されます。

convertible_to[編集]

convertible_toコンセプトは、ある型から他の型への暗黙の変換が可能かどうかを検査します。このコンセプトは、型変換を要求する場合に使用できます。

template<typename From, typename To>
concept C = convertible_to<From, To>; // FromからToへの変換が可能である必要がある

void foo(C<int, double> auto x) { /* ... */ } // xはintからdoubleに変換可能でなければならない

convertible_toは、単なるis_convertibleではありません。実際の変換式が妥当であることも確認しています。

common_reference_with[編集]

common_reference_withコンセプトは、2つの型が共通の参照型を持つかどうかを検査します。このコンセプトは、異なる型の引数を取るジェネリックコードにおいて、共通の型への変換が必要な場合に使用できます。

template<typename T, typename U>
concept C = common_reference_with<T, U>; // TとUには共通の参照型が存在する必要がある

template<C<T, U>>
void foo(T&& t, U&& u) {
  common_reference_t<T&&, U&&> c = std::forward<T>(t); // 共通の参照型cへの変換が可能
  c = std::forward<U>(u); // 逆方向の変換も可能
}

common_reference_withは、common_reference_tと密接に関係しています。共通の参照型の存在と、両方向への変換可能性をチェックしています。

common_with[編集]

common_withコンセプトは、2つの型が共通の型を持つかどうかを検査します。このコンセプトは、異なる型の値を扱うジェネリックコードにおいて、共通の型への変換が必要な場合に使用できます。

template<typename T, typename U>
concept C = common_with<T, U>; // TとUには共通の型が存在する必要がある
    
template<C<T, U>>
auto add(T t, U u) {
  return common_type_t<T, U>(t) + common_type_t<T, U>(u); // 共通の型への変換と演算が可能
}

common_withは、common_type_tと密接に関係しています。共通の型の存在と、両方向への変換可能性、さらに共通の参照型の存在もチェックしています。

これらの言語関連のコンセプトは、テンプレート引数の制約において型の関係性を正確に表現するために重要です。適切なコンセプトを使用することで、コンパイル時のエラー検出が向上し、コードの安全性と理解可能性が高まります。

算術コンセプト[編集]

算術コンセプトは、整数型や浮動小数点数型といった算術型に関する制約を表現するために使用されます。

integral[編集]

integralコンセプトは、その型が整数型であることを要求します。つまり、boolcharshortintlonglong longといった整数型が該当します。

template<integral T>
T abs(T x) {
    return x >= 0 ? x : -x;
}

abs(42);    // OK
abs(3.14);  // エラー: doubleは整数型ではない

整数型であることを要求する関数や算術演算を行うテンプレートでは、integralコンセプトを使用することで、安全性と汎用性が高まります。

signed_integral[編集]

signed_integralコンセプトは、その型が符号付き整数型であることを要求します。つまり、intshortlonglong longなどが該当します。

template<signed_integral T>
T sum(std::vector<T>& v) {
    T result = 0;
    for (T x : v) {
        result += x;
    }
    return result;
}

std::vector<int> v = {1, 2, 3};
int s = sum(v); // OK

符号付き型のみを扱う関数や、オーバーフローのリスクがある演算などでは、signed_integralコンセプトを使用することが重要です。

unsigned_integral[編集]

unsigned_integralコンセプトは、その型が符号なし整数型であることを要求します。つまり、unsigned intunsigned shortunsigned longunsigned long longなどが該当します。

template<unsigned_integral T>
bool is_power_of_two(T x) {
    return (x & (x - 1)) == 0;
}

is_power_of_two(4U);   // OK
is_power_of_two(-2);   // エラー: 負の値は符号なし型に変換できない

ビット演算や、負の値を扱わない場合には、unsigned_integralコンセプトを使用することで、安全性が高まります。

floating_point[編集]

floating_pointコンセプトは、その型が浮動小数点数型であることを要求します。つまり、floatdoublelong doubleなどが該当します。

template<floating_point T>
T compute_area(T radius) {
    return M_PI * radius * radius;
}

double r = 2.5;
double area = compute_area(r); // OK

浮動小数点数型のみを扱う関数や、三角関数、指数関数など数学的な計算を行うテンプレートでは、floating_pointコンセプトを使用することが重要です。

これらの算術コンセプトを適切に使用することで、テンプレートコードの安全性と汎用性が飛躍的に向上します。型の種類に応じた適切な制約を設けることができるため、予期せぬ型エラーを事前に検出でき、コードの堅牢性が高まります。

代入コンセプト[編集]

assignable_from[編集]

assignable_fromコンセプトは、ある型の値が別の型の参照に代入可能であることを要求します。このコンセプトは、代入演算子を定義する際や、関数の引数や戻り値の型制約を設ける場合に使用できます。

struct Base {};
struct Derived : Base {};

void assign(Base& b, Derived& d) {
    b = d; // OK: DerivedはBaseに代入可能
    assignable_from<Base&, Derived&> c; // cは満たされる
}

int x = 42;
int& r = x;
assignable_from<int&, double> c; // エラー: doubleはintに代入できない

assignable_from<LHS, RHS>は、LHS型の参照にRHS型の値を代入できることを確認します。この際、共通の参照型が存在し、その型への変換が行われることも確認されます。

assignable_fromコンセプトを使用することで、代入の安全性が保証され、予期せぬ型エラーを回避できます。代入操作が頻繁に行われるコードでは、このコンセプトの活用が不可欠です。

スワップコンセプト[編集]

スワップコンセプトは、型の値をスワップ(交換)する操作に関する制約を表現するために使用されます。

swappable[編集]

swappableコンセプトは、その型の値がスワップ可能であることを要求します。つまり、ranges::swap関数がその型に対して適用可能であることを意味します。

struct X {
    int value;
    // スワップ操作を定義
    friend void swap(X& a, X& b) noexcept {
        std::ranges::swap(a.value, b.value); 
    }
};

static_assert(swappable<X>); // スワップ可能なので、OK

void f(X a, X b) {
    ranges::swap(a, b); // swappableを満たすため、スワップ可能
}

swappableを満たす型は、ranges::swapを使ってスワップが可能です。カスタム型の場合は、swap関数を適切に定義する必要があります。

swappable_with[編集]

swappable_withコンセプトは、2つの異なる型の値がスワップ可能であることを要求します。

struct Y { int value; };

struct X {
    int value;
    friend void swap(X& a, Y& b) noexcept {
        std::ranges::swap(a.value, b.value);
    }
};

static_assert(swappable_with<X, Y>); // X型とY型はスワップ可能

void f(X a, Y b) {
    ranges::swap(a, b); // swappable_withを満たすため、スワップ可能
}

swappable_withを満たすためには、共通の参照型が存在し、両方向のスワップが可能である必要があります。

ranges::swap[編集]

ranges::swapは、スワップ操作を実行するためのカスタマイゼーションポイントオブジェクトです。swappableswappable_withコンセプトは、このranges::swapが適用可能かどうかを検査しています。

namespace ranges {
inline constexpr unspecified swap = unspecified;
}

ranges::swapは、標準ライブラリ内のスワップ関数と同様の振る舞いをしますが、カスタマイゼーション可能な点が異なります。つまり、ユーザー定義型に対してスワップ操作をカスタマイズすることができます。

これらのスワップコンセプトを適切に使用することで、スワップ操作の安全性が高まり、型の要件を明確に表現できます。また、ユーザー定義型に対するスワップ操作のカスタマイズも容易になります。

破棄と構築コンセプト[編集]

破棄と構築コンセプトは、オブジェクトの生成と破棄に関する制約を表現するために使用されます。

destructible[編集]

destructibleコンセプトは、その型のオブジェクトが安全に破棄可能であることを要求します。つまり、デストラクタが例外を送出しないことを意味します。

struct X {
    ~X() noexcept { /* ... */ }
};

static_assert(destructible<X>); // Xは安全に破棄可能

struct Y {
    ~Y() { /* 例外を送出する可能性がある */ }
};

static_assert(!destructible<Y>); // Yは破棄不可

destructibleを満たす型は、オブジェクトの破棄が安全に行えます。つまり、デストラクタが例外を送出しないことが保証されています。このコンセプトは、コンテナやスマートポインタなど、オブジェクトの生存期間を管理するクラスで重要な役割を果たします。

constructible_from[編集]

constructible_fromコンセプトは、その型のオブジェクトが指定された型の引数から構築可能であることを要求します。

struct X {
    X(int, double) { /* ... */ }
};

static_assert(constructible_from<X, int, double>); // X(int, double)が呼び出せる

struct Y {
    // デフォルトコンストラクタがない
    Y(int) {}
};

static_assert(!constructible_from<Y>); // デフォルトコンストラクタがないため、満たさない

constructible_from<T, Args...>は、T型のオブジェクトがArgs...型の引数から構築可能であることを確認します。このコンセプトは、コンストラクタの要件を明確にし、不適切な呼び出しを防ぐために役立ちます。

default_initializable[編集]

default_initializableコンセプトは、その型のオブジェクトがデフォルト初期化可能であることを要求します。つまり、T obj{};のような構文が使えることを意味します。

struct X {
    X() = default;
};

static_assert(default_initializable<X>); // デフォルトコンストラクタがあるので、OK

struct Y {
    Y(int) {}
};

static_assert(!default_initializable<Y>); // デフォルトコンストラクタがないため、満たさない

default_initializableを満たす型は、デフォルトコンストラクタが存在し、値初期化された状態で安全に使用できることが保証されています。このコンセプトは、コンストラクタの要件を表現するために重要です。

move_constructible[編集]

move_constructibleコンセプトは、その型のオブジェクトが移動構築可能であることを要求します。つまり、一時オブジェクトからの移動コンストラクタが適切に定義されていることを意味します。

struct X {
    X(X&&) = default;
};

static_assert(move_constructible<X>); // 移動コンストラクタがあるので、OK

struct Y { 
    Y(const Y&) = delete;
};

static_assert(!move_constructible<Y>); // コピーコンストラクタがdeleteされているため、満たさない

move_constructibleを満たす型は、一時オブジェクトからの移動構築が可能です。このコンセプトは、リソース管理の最適化や、ムーブセマンティクスを利用したコードの効率化に不可欠です。

copy_constructible[編集]

copy_constructibleコンセプトは、その型のオブジェクトがコピー構築可能であることを要求します。つまり、const参照や非const参照からのコピーコンストラクタが適切に定義されていることを意味します。

struct X {
    X(const X&) = default;
};

static_assert(copy_constructible<X>); // コピーコンストラクタがあるので、OK

struct Y {
    Y(Y&) = delete; // 参照からのコピーコンストラクタがdeleteされている
};

static_assert(!copy_constructible<Y>); // コピー構築が一部不可能なため、満たさない

copy_constructibleを満たす型は、const参照とnon-const参照の両方からコピー構築が可能です。つまり、T(const T&)T(T&)の2つのコンストラクタが適切に定義されている必要があります。また、move_constructibleの要件も満たしている必要があります。

このコンセプトは、値渡しやコピー操作を安全に行うために重要です。コピーコンストラクタの要件を明確にし、不適切な呼び出しを防ぐことができます。

これらの破棄と構築に関するコンセプトは、オブジェクトのライフサイクル管理において非常に重要な役割を果たします。適切なコンセプトを使用することで、安全性と効率性が向上し、コードの信頼性が高まります。また、型の要件を明確に表現できるため、コードの理解可能性も向上します。

比較コンセプト[編集]

比較コンセプトは、値の等価性やオーダリングを表現するのに便利です。

equality_comparable[編集]

equality_comparableは、値が等価性チェック(==演算子)に使用できることを意味します。値が真理値であることが必要なため、この概念は純粋な値の等価性のみを表現します。

std::cout << std::bool_value(true) << std::endl; // bool_value(true): 1と出力。{}を利用する代わりに、純粋な値"true"を利用できます。

std::cout << std::bool_value(false) << std::endl; // bool_value(false): 0と出力。{}は不要です。

common_with[編集]

common_withは、オブジェクトが共通の型を持つことを意味します。これは、共通の型への変換の簡略化と同じ意味を持ちます。

std::cout << std::bool_value(true) << std::endl; // bool_value(true): 1と出力。

std::cout << std::common_value(true,'@'); // common_value(true,'@'): true @となるか、true Uとなる。

totally_ordered[編集]

totally_orderedは、オブジェクトが完全にオーダリングされることを意味します。

std::cout << std::bool_value(true) << std::endl; // bool_value(true): 1と出力。

std::cout << std::common_value(true, '@') << std::endl; // common_value(true, '@'): true @となるか、true Uとなる。{}'は不要です。

equality_comparable_with[編集]

equality_comparable_withは、値が等価性チェック(== 演算子)に使用できることを意味します。これはcommon_withに似ていますが、{}'は不要です。

std::cout << std::equality_comparable_value(true); // equality_comparable_value(true): true Uとなる。

std::cout << std::common_value(true, ' '); // common_value(true, ' '): true となる。{}'は不要

totally_ordered[編集]

totally_orderedは、{}'は不要で直接開閉が使用できる完全にオーダリングされた値を意味します。{}'を含む場合はcommon_withを利用できます。

オブジェクトコンセプト[編集]

オブジェクトコンセプトは、オブジェクトのコピー、ムーブ、スワップ、初期化の可能性を定義します。

movable[編集]

movableコンセプトは、そのオブジェクトがムーブ構築可能で、ムーブ代入可能で、スワップ可能であることを要求します。

struct X {
    X(X&&) = default;
    X& operator=(X&&) = default;
    friend void swap(X&, X&) noexcept { /* ... */ }
};

static_assert(std::movable<X>); // X はムーブ可能

struct Y { /* ... */ };
static_assert(!std::movable<Y>); // ムーブ操作が適切に定義されていない

ムーブセマンティクスを利用したリソース管理の最適化において、movableな型は不可欠です。

copyable[編集]

copyableコンセプトは、そのオブジェクトがコピー構築可能で、ムーブ構築可能で、コピー代入可能で、ムーブ代入可能で、スワップ可能であることを要求します。

struct X {
    X(const X&) = default;
    X& operator=(const X&) = default;
    /* ムーブ操作も定義されている必要がある */
};

static_assert(std::copyable<X>); // X はコピー可能

値渡しやコピー操作を安全に行うために、copyableな型は不可欠です。

semiregular[編集]

semiregularコンセプトは、そのオブジェクトがcopyableで、デフォルト初期化可能であることを要求します。

struct X {
    /* コピー操作の定義 */
    X() = default; 
};

static_assert(std::semiregular<X>); // X は半規則的

semiregularな型は、コンテナやスマートポインタなど、一般的なコードで要求される最低限の条件を満たしています。

regular[編集]

regularコンセプトは、そのオブジェクトがsemiregularで、等価性比較可能であることを要求します。

struct X {
    /* semiregularの要件を満たす */
    bool operator==(const X&) const = default;
};

static_assert(std::regular<X>); // X は規則的

regularな型は、多くのジェネリックコードで安全に使用できることが期待されています。

これらのオブジェクトコンセプトを適切に利用することで、オブジェクトの振る舞いを明確に規定し、安全性と効率性を高めることができます。

呼び出し可能コンセプト[編集]

呼び出し可能コンセプトは、関数オブジェクトや関数ポインタなどの呼び出し可能な型に対する制約を表現します。

invocable[編集]

invocableコンセプトは、その型が指定された引数型リストで呼び出し可能であることを要求します。

struct Fn {
    void operator()(int, double) const {}
};

static_assert(std::invocable<Fn, int, double>); // Fn は (int, double) で呼び出し可能
static_assert(!std::invocable<Fn, int>); // 引数の数が合わない

invocableは、関数呼び出しの可能性をチェックするだけで、結果の型については制約を課しません。

regular_invocable[編集]

regular_invocableコンセプトは、invocableの要件に加えて、呼び出し結果が有効な型であることを要求します。

int fn(int, double) { return 0; }

static_assert(std::regular_invocable<decltype(fn)*, int, double>); // fnは(int, double)で呼び出し可能で、戻り値の型も有効

regular_invocableは、関数呼び出しの結果を安全に使用できることを保証します。

predicate[編集]

predicateコンセプトは、その型が真理値関数(プレディケート)であることを要求します。つまり、呼び出し結果がboolに変換可能である必要があります。

struct IsEven {
    bool operator()(int x) const { return x % 2 == 0; }
};

static_assert(std::predicate<IsEven, int>); // IsEven は int 型の引数に対するプレディケート

プレディケートは、アルゴリズムやコンテナのメンバ関数で広く使用されるため、このコンセプトは重要です。

relation[編集]

relationコンセプトは、その型が2つの引数に対する二項関係(relation)であることを要求します。つまり、プレディケートの要件を満たすと同時に、引数の順序が入れ替わっても呼び出し可能である必要があります。

struct Compare {
    bool operator()(int a, int b) const { return a < b; }
};

static_assert(std::relation<Compare, int, int>); // Compare は int 型の引数に対する二項関係

比較関数などでは、relationの要件を満たす必要があります。

equivalence_relation[編集]

equivalence_relationコンセプトは、その型が同値関係(equivalence relation)であることを要求します。これは、relationの要件に加えて、同値性(reflexivity)、対称性(symmetry)、推移性(transitivity)を満たす必要があります。

struct Equal {
    bool operator()(int a, int b) const { return a == b; }
};

static_assert(std::equivalence_relation<Equal, int, int>); // Equal は int 型の同値関係

集合操作や連想コンテナのキーの比較などでは、equivalence_relationの要件を満たす必要があります。

strict_weak_order[編集]

strict_weak_orderコンセプトは、その型が弱い全順序関係(strict weak ordering relation)であることを要求します。これは、relationの要件に加えて、非反射性(irreflexivity)、推移性(transitivity)、全順序性(総順序関係、total ordering)を満たす必要があります。

struct Less {
    bool operator()(int a, int b) const { return a < b; }
};

static_assert(std::strict_weak_order<Less, int, int>); // Less は int 型の弱い全順序関係

ソートやマージ操作などでは、strict_weak_orderの要件を満たす比較関数が必要とされます。

これらの呼び出し可能コンセプトを適切に利用することで、関数オブジェクトや関数ポインタの要件を明確に表現でき、安全性と合理性が向上します。

特にpredicaterelationequivalence_relationstrict_weak_orderは、アルゴリズムやコンテナで広く使用されるため、これらのコンセプトを理解しておくことが重要です。呼び出し可能な型の要件を正しく定義することで、不適切な使用を防ぎ、コードの品質を高めることができます。

さらに、invocableregular_invocableは一般的な呼び出し可能性のチェックに役立ちます。regular_invocableを使うことで、呼び出し結果の型の有効性も確認できるため、安全性がさらに高まります。

呼び出し可能コンセプトは、ジェネリックプログラミングの根幹をなす概念です。これらのコンセプトを上手く活用することで、型安全で効率的なジェネリックコードを記述することが可能になります。また、コンセプトを利用することで、コードの意図がより明確になり、可読性と保守性が向上します。

まとめ[編集]

C++20から導入された<concepts>ヘッダーは、ジェネリックプログラミングの領域に大きな変化をもたらしました。このヘッダーが提供するコンセプトにより、型の制約を明確に表現することができるようになりました。

コンセプトは、型が満たすべき条件や、その型に対して適用可能な操作を定義するものです。これにより、テンプレートの型パラメータに対する制約を簡潔かつ明示的に記述できるようになりました。

本章では、<concepts>ヘッダーで提供される様々なコンセプトについて詳しく説明しました。言語関連のコンセプトでは、型の同一性や派生関係、変換可能性などを扱いました。算術コンセプトでは、整数型や浮動小数点数型への制約を表現する方法を学びました。また、代入可能性や破棄・構築可能性、スワップ可能性など、オブジェクトのライフサイクルに関わるコンセプトも紹介しました。

さらに、比較コンセプトでは等価性や順序関係を表す概念を、呼び出し可能コンセプトでは関数オブジェクトの要件を定義する方法を学びました。

適切なコンセプトを使用することで、以下の利点が得られます。

  • コンパイル時のエラー検出が向上し、型安全性が高まる
  • コードの意図が明確になり、可読性と保守性が向上する
  • ジェネリックコードの汎用性と再利用性が高まる
  • コンセプトを満たすユーザー定義型も簡単に扱える

<concepts>ヘッダーはC++20の主要な機能の1つであり、今後のジェネリックプログラミングにおいて欠かせない存在となるでしょう。本章で学んだコンセプトを活用することで、より安全で効率的なコーディングが可能になります。C++プログラマーとして、これらの概念を理解し、適切に利用することが求められます。