Closed39

C++20のコンセプトについて

Toru3Toru3

今まではSFINAEで頑張って1つだけ有効になるように調整していたのが、制約を書くだけで良くなる?

Toru3Toru3

requires節を使うと複数制約書ける。(Rustのwhereと似ている?)

Toru3Toru3

関数のオーバーロードとしては、より制約が強い関数が選択される(より強いとは?)

Toru3Toru3

明示的にそうだと書かれているところはまだ見つけていないが、
C1、C2を制約としてC1C1 && C2 では後者の方が制約が強いとみなされ、 C1C1 || C2 では前者の方が制約が強いとみなされそう。これらの形式に当てはまらない場合は制約の強さは比較不能となるのではないかと推測される。

Toru3Toru3

C1、C2を制約としてC1 と C1 && C2 では後者の方が制約が強いとみなされ、 C1 と C1 || C2 では前者の方が制約が強いとみなされそう。

これは合っているが、これ以外の場合でも順序がつく場合がありそう。

Toru3Toru3

半順序を考えるときに ! が論理演算ではなく原子制約であることに注意しないといけないと思われる。

例えば !(C1 && C2) はこれそのものが原子制約であって、!C1 || !C2 とは異なるはず。(つまり、conceptsではド・モルガンの法則が成り立たない)
よって、!(C1 && C2)!C1 は比較不能になると思われる。

Toru3Toru3
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が呼ばれるはず。
https://wandbox.org/permlink/v2kb8ANVyq27Wsj9

Toru3Toru3

ちなみに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);
}

https://wandbox.org/permlink/puzOv8tf1S4TpiKf

Toru3Toru3

C++0xの時点で入れようとしていたのだと

制約外の機能を使用禁止 : コンセプトで制約した機能以外はテンプレート内で使用できない。Destructibleコンセプトで制約しなければオブジェクトの破棄すらできない

というのものだったらしい。(Rustのトレイト境界と似ている。)

Toru3Toru3

https://cpprefjp.github.io/reference/concepts/totally_ordered.html にはdoubleがtotally_orderedと書いてあるし、https://wandbox.org/permlink/fq262AyPh6Jk1UPz でもtotally_orderedと返してくるが、NaNとの比較は<, ==, > のいずれもfalseになるのでtotally_orderedではないはず。

Toru3Toru3

もしかして std::equivalence_relation は同値関係であるかチェックしてない?

Toru3Toru3

制約の否定は論理演算として扱われないので、同じ式に対する否定が同じものとして扱われない。

Toru3Toru3

!hoge<T> が別の場所で出てきたらそれらが同じものであると認識してくれない。hoge<T> だったら同じものと認識してくれる。また、template<class T> concept not_hoge = !hoge<T>; とした上でnot_hoge<T> を使うと同じものだと認識される。

Toru3Toru3
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の制約はC1C1&&C2ではなくC1C2&&C3として扱われてしまうため、優先順位が付けられない。そのためf1の呼び出しは曖昧となるという解釈で良いと思われる。

Toru3Toru3
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の制約はC1C1 && C2となるためf<0>()の呼び出しは制約の強い後者が優先されて返り値の型はintとなるという解釈で良いと思われる。
Add1TyとAddOneTyは同じものとみなされるためgが2回宣言されているが、同一内容での再宣言とみなされ、gの呼び出しは曖昧にならないという解釈で良いと思われる。

Toru3Toru3
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*NN*2も同じ結果になるが、Add1<2*N>Add1<N*2>は同じとみなされないため、f2の制約はC1C2 && C3とみなされ優先順位がつかないためill-formedになると思われる。

Toru3Toru3

パラメーター置換とテンプレート引数への代入の失敗はconceptが満たされないと判定される。(N4861 13.5.1.2.3)(テンプレートに対するSFINAEと同様にコンパイルエラーにはならない。候補から外されるのではなく制約を満たさないと扱われるのが違いだと思われる。)

Toru3Toru3

原子制約(E)はbool型の定数式になり、「E==true ⇔ 制約を満たす」と定義されている。(N4861 13.5.1.2.3)

「制約を満たさないならばE==false」は成り立たない。なぜなら前述の通りテンプレート引数の代入失敗などの場合にも満たされないため。

Toru3Toru3
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は何のためにあるのか分からない。単なる原子制約の例ってことか?)

Toru3Toru3
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]

Toru3Toru3

コンセプトの定義

concept-definition:
	concept concept-name = constraint-expression ;

concept-name:
	identifier

N4861 13.7.8 より

  • コンセプト定義では関連制約(多分typenameやclassの代わりに制約書けるあれ)は使えない。
  • コンセプトはインスタンス化されない。(特殊化や部分的な特殊化は出来ない。)
  • 制約式は評価されない。
  • 最初のテンプレートパラメーターはプロトタイプパラメーターという。型に対するコンセプトはプロトタイプパラメーターに対するもの。
Toru3Toru3

関連制約は使えないは上で書いたのは不完全で、構文的には

template<C1 T> requires { /*some expression*/ }
concept C2 = CEXPR;

みたいにTに対する制約をCEXPR以外にも書けるが、コンセプトの定義ではこれを禁止している。ということだと思われる。

Toru3Toru3

なので、テンプレートパラメーターが1つの場合は

template<typename T>
concept CNAME = CEXPR;

のような定義しか許されないのではないかと思ったが、テンプレートテンプレートとかは許されそうなので、そうでもない?

Toru3Toru3

clangとgccのHEADではコンセプト定義にテンプレートテンプレートパラメーター使えた。
どちらも template<C1 T> concept C2 = true; はエラーになった。
この挙動は規格通りだと思われる。

このスクラップは2021/02/14にクローズされました