yohhoyの日記

技術的メモをしていきたい日記

複合要件とsame_as/convertible_toコンセプト

C++2a(C++20)コンセプト requires式(requires-expression) に記述する 複合要件(compound-requirement) では「ある式の評価結果が特定コンセプトを満たすこと」を制約するが、このとき標準コンセプトstd::same_asstd::convertible_toを適切に使い分ける必要がある。特に “データメンバ型の制約” には注意すること。

下記コードのコンセプトC0では式t.dataint型へと変換可能(convertible_to<int>)と制約しているが、コンセプトC1のようにint型と等しい(same_as<int>)と制約するとプログラマの期待通りに動作しない。

// C++2a
#include <concepts>

struct S {
  int data;   // int型のデータメンバ
  int& mf();  // int&型を返すメンバ関数
};

template <typename T> concept C0 = requires (T t) {
  // T型の値tはデータメンバdataをもち、int型へと変換可能
  { t.data } -> std::convertible_to<int>;
  // T型の値tはメンバ関数mf()をもち、呼び出し結果はint&型に等しい
  { t.mf() } -> std::same_as<int&>;
};
static_assert( C0<S> );  // OK

template <typename T> concept C1 = requires (T t) {
  // T型の値tはデータメンバdataをもち、"式t.data"はint型に等しい
  { t.data } -> std::same_as<int>;
  // ...
};
static_assert( C1<S> );  // NG: 式t.dataの型はint&

戻り値型を制約する複合要件{ E } -> C<U>;は、2つの制約E; requires C<decltype((E)), U>;*1と等価である。ここでdecltype指定子オペランドが括弧付きの式(E)となることに留意。(→id:yohhoy:20200817

template <typename T> concept C1 = requires (T t) {
  // { t.data } -> std::same_as<int>; と等価な制約
  t.data;
  requires std::same_as<decltype((t.data)), int>;
  // ...
};

クラスのデータメンバ型をsame_asコンセプトで制約する場合は、下記コンセプトC2のように入れ子要件(nested-requirement)を利用する。*2

template <typename T> concept C2 = requires {
  // メンバT::dataはint型に等しい
  requires std::same_as<decltype(T::data), int>;
  // ...
}
static_assert( C2<S> );  // OK

下記コンセプトC3のように複合要件とsame_asコンセプトを組み合わせた場合、データメンバ型はintまたはint&いずれも制約を満たす。これはsame_asコンセプト利用意図からすると、不適切な制約表現といえる。*3

struct S1 {
  int data;   // int型のデータメンバ
};
struct S2 {
  int& data;  // int&型のデータメンバ
};

template <typename T> concept C3 = requires (T t) {
  { t.data } -> std::same_as<int&>;
};

// OK: データメンバが int型 または int&型 であれば制約を満たす
static_assert( C3<S1> && C3<S2> ); 

N4861 7.5.7.3/p1, 18.4.2, 18.4.4/p1より一部引用。

compound-requirement:
  { expression } noexceptopt return-type-requirementopt ;
return-type-requirement:
  -> type-constraint


A compound-requirement asserts properties of the expression E. Substitution of template arguments (if any) and verification of semantic properties proceed in the following order:

  • Substitution of template arguments (if any) into the expression is performed.
  • If the noexcept specifier is present, E shall not be a potentially-throwing expression (14.5).
  • If the return-type-requirement is present, then:
    • Substitution of template arguments (if any) into the return-type-requirement is performed.
    • The immediately-declared constraint (13.2) of the type-constraint for decltype((E)) shall be satisfied. [Example: Given concepts C and D,
requires {
  { E1 } -> C;
  { E2 } -> D<A1, ..., An>;
};

is equivalent to

requires {
  E1; requires C<decltype((E1))>;
  E2; requires D<decltype((E2)), A1, ..., An>;
};

(including in the case where n is zero). -- end example]

template<class T, class U>
  concept same-as-impl = is_same_v<T, U>;  // exposition only
template<class T, class U>
  concept same_as = same-as-impl <T, U> && same-as-impl <U, T>;

[Note: same_as<T, U> subsumes same_as<U, T> and vice versa. -- end note]

Given types From and To and an expression E such that decltype((E)) is add_rvalue_reference_t<From>, convertible_to<From, To> requires E to be both implicitly and explicitly convertible to type To. The implicit and explicit conversions are required to produce equal results.

template<class From, class To>
  concept convertible_to =
    is_convertible_v<From, To> &&
    requires(add_rvalue_reference_t<From> (&f)()) {
      static_cast<To>(f());
    };

関連URL

*1:単純要件(simple-requirement)と入れ子要件(nested-requirement)の2つで表現される。

*2:制約が std::same_as<decltype(T::data), int> のみで構成される場合は requires 式の入れ子要件とする必要はなく、直接 template <typename T> concept C2 = std::same_as<decltype(T::data), int>; と記述すればよい。

*3:ソースコード上は “参照型と等しい” と読み取れる記述がなされ、実際には参照型/値型の両方を許容するコンセプトはトラブルの元になる可能性が高い。