yohhoyの日記

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

可変長コンセプト×畳み込み式: The glass is half full or half empty?

C++20コンセプトと論理演算子(&&, ||)による畳み込み式(fold expression)の関係について。本記事の内容はStackOverflowで見つけた質問と回答に基づく。

まとめ:&&||による畳み込み式を用いた制約式(constraint-expression)は機能するものの、コンセプト間の包摂関係(subsumption relation)は期待通りに成り立たない。

2023-10-10追記:C++2cに向けた提案P2963R0にて、&&||畳み込み式を用いた場合でも包摂関係が成り立つよう言語仕様を調整する検討が行われている。
2024-07-21追記:C++2c(C++26)標準ライブラリでは提案文書(PDF)P2963R3が採択されたが*1、本記事のケースはサポートしない。畳み込み式が関与するより広範なケースは提案P2841にて継続議論されている。

#include <concepts>

// コンセプト any_of<T, Us...>
// 型Tが型リストUs..のいずれかと一致する?
template <typename T, typename... Us>
concept any_of = (std::same_as<T, Us> || ...);

template <typename T>
constexpr int f(T) { return 1; }  // #1

template <typename T>
  requires any_of<T, int, double>
constexpr int f(T) { return 2; }  // #2
// または
template <any_of<int, double> T> 
constexpr int f(T) { return 2; }  // #2

static_assert(f(42) == 2);    // OK: #2 int型
static_assert(f(3.14) == 2);  // OK: #2 double型
static_assert(f(0.f) == 1);   // OK: #1 float型
static_assert(f('X') == 1);   // OK: #1 char型

C++20コンセプト仕様では、畳み込み式(std::same_as<T, Us> || ...)それ自体で一つの原始制約(atomic constraint)を構成する。オーバーロード解決のために制約式の包摂関係を求める(≒強弱の判定)正規化(normalization)過程で、std::same_as<T, Us[0]>std::same_as<T, Us[1]>...のようには展開解釈されない。*2

// 畳み込み式による(可変長)コンセプト定義
template <typename T, typename... Us>
concept any_of = (std::same_as<T, Us> || ...);

template <any_of<int, double> T> 
constexpr int f(T) { return 2; }  // #2

template <std::same_as<double> T>
constexpr int f(T) { return 3; }  // #3

static_assert(f(42) == 2);    // OK: #2の制約のみ満たす
static_assert(f(3.14) == 3);  // NG: #2,#3間でオーバロード解決が曖昧

可変長コンセプトをあきらめて制約式(std::same_as<T, U1> || std::same_as<T, U2>)とすれば包摂関係が成立し、自然なオーバーロード選択が行われる。つまり制約any_of2<T, int, double>よりも制約std::same_as<T, double>の方がより強い制約(more constrained)と解釈される。

// 畳み込み式を利用しない(2型版)コンセプト定義
template <typename T, typename U1, typename U2>
concept any_of2 = (std::same_as<T, U1> || std::same_as<T, U2>);

template <any_of2<int, double> T> 
constexpr int f(T) { return 2; }  // #2

template <std::same_as<double> T>
constexpr int f(T) { return 3; }  // #3

static_assert(f(42) == 2);    // OK: #2
static_assert(f(3.14) == 3);  // OK: #3は#2より強く制約される

関連URL

*1:https://github.com/cplusplus/papers/issues/1638

*2:C++20 §13.5.1.1/p1 Note: "(snip) For the purpose of exposition, conjunction is spelled using the symbol ⋀ and disjunction is spelled using the symbol ⋁. (snip)"