C++20のコンセプトについて
コンセプトがどう嬉しいかの説明
今まではSFINAEで頑張って1つだけ有効になるように調整していたのが、制約を書くだけで良くなる?
requires節を使うと複数制約書ける。(Rustのwhereと似ている?)
関数のオーバーロードとしては、より制約が強い関数が選択される(より強いとは?)
明示的にそうだと書かれているところはまだ見つけていないが、
C1、C2を制約としてC1
と C1 && C2
では後者の方が制約が強いとみなされ、 C1
と C1 || C2
では前者の方が制約が強いとみなされそう。これらの形式に当てはまらない場合は制約の強さは比較不能となるのではないかと推測される。
13.5.4が制約による半順序
C1、C2を制約としてC1 と C1 && C2 では後者の方が制約が強いとみなされ、 C1 と C1 || C2 では前者の方が制約が強いとみなされそう。
これは合っているが、これ以外の場合でも順序がつく場合がありそう。
半順序を考えるときに !
が論理演算ではなく原子制約であることに注意しないといけないと思われる。
例えば !(C1 && C2)
はこれそのものが原子制約であって、!C1 || !C2
とは異なるはず。(つまり、conceptsではド・モルガンの法則が成り立たない)
よって、!(C1 && C2)
と !C1
は比較不能になると思われる。
template<typename T> concept C1 = false;
template<typename T> concept C2 = false;
template<typename T> requires (!(C1<T> && C2<T>)) void f(T); //#1
template<typename T> requires (!C1<T>) void f(T); //#2
template<typename T> concept notC1 = !C1<T>;
template<typename T> concept notC2 = !C2<T>;
template<typename T> requires notC1<T> || notC2<T> void g(T); //#3
template<typename T> requires notC1<T> void g(T); //#4
void func() {
f(0);
g(0);
}
上はfは曖昧になり、gは(曖昧にならず)#4が呼ばれるはず。
ちなみにnotC1を定義しないと https://zenn.dev/toru3/scraps/94bf999122880b#comment-52e95cfafc61ba の理由で同じ制約とはみなされないため、順序がつかない。つまりhの呼び出しは曖昧になる。
template<typename T> concept C1 = false;
template<typename T> concept C2 = false;
template<typename T> requires (!(C1<T> && C2<T>)) void f(T); //#1
template<typename T> requires (!C1<T>) void f(T); //#2
template<typename T> concept notC1 = !C1<T>;
template<typename T> concept notC2 = !C2<T>;
template<typename T> requires notC1<T> || notC2<T> void g(T); //#3
template<typename T> requires notC1<T> void g(T); //#4
template<typename T> requires (!C1<T> || !C2<T>) void h(T); //#5
template<typename T> requires (!C1<T>) void h(T); //#6
void func() {
f(0);
g(0);
h(0);
}
C++0xの時点で入れようとしていたのだと
制約外の機能を使用禁止 : コンセプトで制約した機能以外はテンプレート内で使用できない。Destructibleコンセプトで制約しなければオブジェクトの破棄すらできない
というのものだったらしい。(Rustのトレイト境界と似ている。)
ヘッダ
semiregularかつregularでないのは、==
で比較できないとき。(float
, double
はregular)
対称律、推移律は課しているが、反射律は課していないのでfloat
もequality_comparable。
https://cpprefjp.github.io/reference/concepts/totally_ordered.html にはdouble
がtotally_orderedと書いてあるし、https://wandbox.org/permlink/fq262AyPh6Jk1UPz でもtotally_orderedと返してくるが、NaNとの比較は<
, ==
, >
のいずれもfalseになるのでtotally_orderedではないはず。
a<b
,a==b
, a>b
のうちちょうど1つがtrueになると書いてあるので、浮動小数点数型はNaNがこれを満たさないので、やはりtotally_orderedでないはず。
https://timsong-cpp.github.io/cppwp/n4861/concept.totallyordered の(1.1)を見ると、
Exactly one of bool(a < b), bool(a > b), or bool(a == b) is true.
と書かれている。
partially-ordered-withの定義は
と の違いは何?
もしかして std::equivalence_relation は同値関係であるかチェックしてない?
制約の否定は論理演算として扱われないので、同じ式に対する否定が同じものとして扱われない。
!hoge<T>
が別の場所で出てきたらそれらが同じものであると認識してくれない。hoge<T>
だったら同じものと認識してくれる。また、template<class T> concept not_hoge = !hoge<T>;
とした上でnot_hoge<T>
を使うと同じものだと認識される。
template <class T> concept sad = false;
template <class T> int f1(T) requires (!sad<T>);
template <class T> int f1(T) requires (!sad<T>) && true;
int i1 = f1(42); // ambiguous, !sad<T> atomic constraint expressions ([temp.constr.atomic])
// are not formed from the same expression
template <class T> concept not_sad = !sad<T>;
template <class T> int f2(T) requires not_sad<T>;
template <class T> int f2(T) requires not_sad<T> && true;
int i2 = f2(42); // OK, !sad<T> atomic constraint expressions both come from not_sad
template <class T> int f3(T) requires (!sad<typename T::type>);
int i3 = f3(42); // error: associated constraints not satisfied due to substitution failure
template <class T> concept sad_nested_type = sad<typename T::type>;
template <class T> int f4(T) requires (!sad_nested_type<T>);
int i4 = f4(42); // OK, substitution failure contained within sad_nested_type
N4861 13.5.1.1より引用
!
は論理演算ではなく原子制約として扱われる。そのため上のf1の!sad<T>
と!sad<T> && true
の!sad<T>
は実態としては同じものを表しているが、制約としては別のものとして扱われる。よってf1の制約はC1
とC1&&C2
ではなくC1
とC2&&C3
として扱われてしまうため、優先順位が付けられない。そのためf1の呼び出しは曖昧となるという解釈で良いと思われる。
yohhoyさんの記事
C++ Concepts(P0734R0)
C++ Conceptsの短縮構文(P1141R2)
定数式を要求するコンセプト
C++標準コンセプトの名前付けガイドライン
コンセプト制約式の包摂関係とオーバーロード解決
same_asコンセプトとSymmetric Subsumption Idiom
C++20標準ライブラリ仕様:Constraints/Mandates/Preconditions
requires式から利用可能な宣言
C++コンセプトとド・モルガンの法則
複合要件とsame_as/convertible_toコンセプト
Concept-basedオーバーロードとSFINAE-unfriendlyメタ関数の落とし穴 ※遡及適応あり
コンセプトと短絡評価
コンセプトのパラメータ置換失敗はハードエラーではない
template <unsigned N> constexpr bool Atomic = true;
template <unsigned N> concept C = Atomic<N>;
template <unsigned N> concept Add1 = C<N + 1>;
template <unsigned N> concept AddOne = C<N + 1>;
template <unsigned M> void f()
requires Add1<2 * M>;
template <unsigned M> int f()
requires AddOne<2 * M> && true;
int x = f<0>(); // OK, the atomic constraints from concept C in both fs are Atomic<N>
// with mapping similar to N↦2 * M + 1
template <unsigned N> struct WrapN;
template <unsigned N> using Add1Ty = WrapN<N + 1>;
template <unsigned N> using AddOneTy = WrapN<N + 1>;
template <unsigned M> void g(Add1Ty<2 * M> *);
template <unsigned M> void g(AddOneTy<2 * M> *);
void h() {
g<0>(nullptr); // OK, there is only one g
}
N4861 13.5.1.2より引用
上のAdd1とAddOneは同じものとしてみなされるため、fの制約はC1
とC1 && C2
となるためf<0>()
の呼び出しは制約の強い後者が優先されて返り値の型はintとなるという解釈で良いと思われる。
Add1TyとAddOneTyは同じものとみなされるためgが2回宣言されているが、同一内容での再宣言とみなされ、gの呼び出しは曖昧にならないという解釈で良いと思われる。
template <unsigned N> void f2()
requires Add1<2 * N>;
template <unsigned N> int f2()
requires Add1<N * 2> && true;
void h2() {
f2<0>(); // ill-formed, no diagnostic required:
// requires determination of subsumption between atomic constraints that are
// functionally equivalent but not equivalent
}
N4861 13.5.1.2より引用
実質同等であるが、同一とみなされない例。unsigned int型において2*N
もN*2
も同じ結果になるが、Add1<2*N>
とAdd1<N*2>
は同じとみなされないため、f2の制約はC1
とC2 && C3
とみなされ優先順位がつかないためill-formedになると思われる。
パラメーター置換とテンプレート引数への代入の失敗はconceptが満たされないと判定される。(N4861 13.5.1.2.3)(テンプレートに対するSFINAEと同様にコンパイルエラーにはならない。候補から外されるのではなく制約を満たさないと扱われるのが違いだと思われる。)
原子制約(E
)はbool型の定数式になり、「E==true ⇔ 制約を満たす」と定義されている。(N4861 13.5.1.2.3)
「制約を満たさないならばE==false」は成り立たない。なぜなら前述の通りテンプレート引数の代入失敗などの場合にも満たされないため。
template<typename T> concept C =
sizeof(T) == 4 && !true; // requires atomic constraints sizeof(T) == 4 and !true
template<typename T> struct S {
constexpr operator bool() const { return true; }
};
template<typename T> requires (S<T>{})
void f(T); // #1
void f(int); // #2
void g() {
f(0); // error: expression S<int>{} does not have type bool
} // while checking satisfaction of deduced arguments of #1;
// call is ill-formed even though #2 is a better match
N4861 13.5.1.2より引用
普通なら(テンプレートではない)関数#2が優先されるが、#1の宣言の中で制約がbool型を返していないのでfの呼び出しそのものがill-formedとなっている。
補足 : S<T>{}
はbool型の定数式に変換可能であるが、bool型の定数式そのものではないため駄目。#1の制約を bool(S<T>{})
とすればbool型への変換を明示的に呼んでいるのでこれはbool型の定数式となり、(ill-formedとならず)#2が呼ばれる。
(Cは何のためにあるのか分からない。単なる原子制約の例ってことか?)
template <C1 T> requires C2<T> void f1(T);
template <typename T> requires C1<T> && C2<T> void f2(T);
f1, f2の制約はC1<T>
∧ C2<T>
。(C2<T>
∧ C1<T>
ではない。) [N4861 13.5.2]
13.5はとりあえず読めた。
コンセプトの定義
concept-definition:
concept concept-name = constraint-expression ;
concept-name:
identifier
N4861 13.7.8 より
- コンセプト定義では関連制約(多分typenameやclassの代わりに制約書けるあれ)は使えない。
- コンセプトはインスタンス化されない。(特殊化や部分的な特殊化は出来ない。)
- 制約式は評価されない。
- 最初のテンプレートパラメーターはプロトタイプパラメーターという。型に対するコンセプトはプロトタイプパラメーターに対するもの。
関連制約は使えないは上で書いたのは不完全で、構文的には
template<C1 T> requires { /*some expression*/ }
concept C2 = CEXPR;
みたいにTに対する制約をCEXPR以外にも書けるが、コンセプトの定義ではこれを禁止している。ということだと思われる。
なので、テンプレートパラメーターが1つの場合は
template<typename T>
concept CNAME = CEXPR;
のような定義しか許されないのではないかと思ったが、テンプレートテンプレートとかは許されそうなので、そうでもない?
clangとgccのHEADではコンセプト定義にテンプレートテンプレートパラメーター使えた。
どちらも template<C1 T> concept C2 = true;
はエラーになった。
この挙動は規格通りだと思われる。