yohhoyの日記

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

C++コンセプトとド・モルガンの法則

C++2a(C++20)コンセプトでは制約式(constraint-expression)に論理積(&&)/論理和(||)/論理否定(!)を表現できるが、制約式を用いたオーバーロード解決では通常の論理演算で期待される ド・モルガンの法則(De Morgan's laws) は適用されない。*1
超要約:否定演算!を使ったコンセプトには要注意!

// C++2a
#include <concepts>  // integral, same_asコンセプト

template <typename T>
  requires (!std::integral<T>)
void f(T) { /*...*/ }  // #1

template <typename T>
  requires (!std::integral<T> && std::same_as<T, float>)
void f(T) { /*...*/ }  // #2

f(3.14 );  // OK: #1を選択(T=double)
f(3.14f);  // NG: オーバーロード解決が曖昧(T=float)

2番目の関数呼び出し(T=float)では関数fの#1, #2両オーバーロードとも制約式を満たすが、包摂関係(subsumption relation)は成り立たないためオーバーロード解決が曖昧となる。これは#1, #2の制約式に含まれる部分式!std::integral<T>が、それぞれ異なる原始制約(atomic constraint)と解釈されるため。(「・ω・)「

後述例のように、コンセプトnot_integralを介して “同一の式から構成される原始制約” とすれば期待通り動作する。ここでは#2の制約式not_integral<T> && std::same_as<T, float>は#1の制約式not_integral<T>を包摂(subsume)している。(」・ω・)」

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

template <typename T>
concept not_integral = !std::integral<T>;

template <typename T>
  requires not_integral<T>
void f(T) { /*...*/ }  // #1

template <typename T>
  requires not_integral<T> && std::same_as<T, float>
void f(T) { /*...*/ }  // #2

f(3.14 );  // OK: #1を選択(T=double)
f(3.14f);  // OK: #2を選択(T=float)

C++2a DIS n4861 13.5.1.1/p5より引用(下線部は強調)。注:13.7.6.1=Function template overloading, 13.5.4=Partial ordering by constraints

[Note: A logical negation expression (7.6.2.1) is an atomic constraint; the negation operator is not treated as a logical operation on constraints. As a result, distinct negation constraint-expressions that are equivalent under 13.7.6.1 do not subsume one another under 13.5.4. Furthermore, if substitution to determine whether an atomic constraint is satisfied (13.5.1.2) encounters a substitution failure, the constraint is not satisfied, regardless of the presence of a negation operator. [Example:

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 (13.5.1.2)
                  // 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

Here, requires (!sad<typename T::type>) requires that there is a nested type that is not sad, whereas requires (!sad_nested_type<T>) requires that there is no sad nested type. --end example] --end note]

関連URL