パターンマッチングの提案(P1371R3)を眺める(WIP)
※注意 : この記事はまだ執筆中です。タイトルの WIP が外れることを願っています。
個人的に気になったので、 P1371R3 : Pattern Matching についてメモ。
基本的に雑な和訳ですが、分かり易さのために改変したり注や例を挟むことがあります。
動機と領域
まず、既存の switch
や if
などの条件分岐の構文では、単体の整数や真偽値による判定しかできず、標準ライブラリの提供する様々な型 (原文 : "vocabulary types") たちの検査には不十分です。
C++17 では構造化束縛宣言が導入され、tuple-like な型の要素を名前に束縛することができるようになりました。この提案では inspect 式により 検査束縛 (原文 : structured inspection) を行うことで構造化束縛の概念を自然に拡張することを目指しています。
デザイン概要
基本文法
inspect 式の基本的な文法は以下のようになります。1
inspect constexpropt ( init-statementopt condition ) trailing-return-typeopt {
pattern guardopt => statement
pattern guardopt => !opt { statement-seq }
. . .
}
guard:
if ( expression )
基本モデル
inspect 式の丸括弧の中は switch
や if
の丸括弧の中と基本的に等価ですが、変換も整数昇格も行われません。
筆者例
char c; // 整数昇格が発生 (char -> int) switch (c) { /* ... */ } // 変換が発生 (char -> bool) if (c) { /* ... */ } // char のまま処理される inspect (c) { /* ... */ }; // 丸括弧の中身は switch や if と同じ inspect (int m = c; long n = m) { /* ... */ };
inspect
は全ての文脈において式であり、内包された文に依存して void
または値を返します。値を返す場合、その型は内包された文から静的に推定されるか、 trailing return type によって指定されます。 pattern の返す型は全て一致している必要があります (原文 : the return types of all
patterns must match) が、もし trailing return type が提供されている場合は暗黙に変換できる型である必要があります。 (この推定はラムダ式の戻り値の推定と類似しています。)
また、複合文へと制御を移す pattern は void
を返します。
筆者例:
inspect (/* ... */) { /* ... */ }; // セミコロン必須! ^^^ int n; // OK, x の型は int auto x = inspect (n) { 0 => 314; 1 => 271; __ => 42; }; // NG, 型が異なる auto y = inspect (n) { 0 => 3.14; 1 => 2.71; __ => 42; }; // OK, z の型は double auto z = inspect (n) -> double { 0 => 3.14; 1 => 2.71; __ => 42; };
もし !
接頭辞が複合文の前に現れた場合、その文は inspect 式の戻り値型の推定に寄与しません。そのような文は値を返すことが期待されておらず、 inspect 式を内包する関数から戻る、例外を投げる、プログラムを中断する、のいずれかによって処理を中断しなければいけません。これによってユーザーは、マッチがない場合の挙動や望まないマッチに対する挙動を inspect 式の結果に影響を与えることなく表現することができます。もし制御がこの複合文の最後に到達した場合、 std::terminate
が呼ばれます。
筆者例:
// 提案書に記載されている例 enum class Op { Add, Sub, Mul, Div }; Op parseOp(Parser& parser) { return inspect (parser.consumeToken()) { '+' => Op::Add; '-' => Op::Sub; '*' => Op::Mul; '/' => Op::Div; token => !{ std::cerr << "Unexpected: " << token; std::terminate(); } // 筆者注:ここでは明示的に std::terminate を呼んでいるが、 // 呼ばずとも複合文の終了時に std::terminate が呼ばれる }; } // 筆者例 std::string func(int n) { return std::to_string( inspect (n) { // return により関数から脱出 0 => !{ return "zero"; } // 例外を投げる 1 => !{ throw std::logic_error("one"); } // 上2つは inspect 式の結果の型に影響しない v => v; } ); }
inspect 式が実行されると、まず condition
が評価され、パターンに対し現れた順にマッチが行われます。あるパターンがこのマッチに成功し、かつ guard
内の式が true
に評価された場合 (または guard
が存在しない場合) 、式の値が返るか、複合文に制御が移されます (これはその検査が値を返すかどうかに依存します) 。もし guard
内の式が false
に評価された場合、制御は後続のパターンに進みます。
筆者注:後述の筆者例を参照してください
もしいずれのパターンにもマッチしなかった場合、式や複合文のいずれも実行されません。このとき、 inspect 式が void
を返す場合は制御は次の文に移ります。そうでなく inspect 式が void
を返さない場合、 std::terminate
が呼ばれます。
void f(int n) { std::cout << "try inspection..." << std::endl; inspect (n) { // 常にマッチしない v if ( false ) => { std::cout << "never match" << std:endl; } }; // void が返るので制御は次の文へ std::cout << "inspection done" << std::endl; } int g(int n) { return inspect(n) { // 常にマッチしない v if ( false ) => 42; }; // int を返すべきだが、マッチなしなので std::terminate が呼ばれる }
パターンの種類
Primary Patterns
Wildcard Pattern
ワイルドカードパターンは以下の形式を持ち、
__
どんな値 v
にもマッチします。
int v = /* ... */; inspect (v) { __ => { std::cout << "matches any value" << std::endl; } // ^^ wildcard pattern };
この提案書ではワイルドカードの識別子として __
を採用しています。この提案書の筆者はワイルドカードとして _
を予約しようと試みましたが、 EWG に固く反対されました。
Identifier Pattern
識別子パターンは以下の形式を持ち、
identifier
どんな値 v
にもマッチします。この identifier
は v
を参照する lvalue
として振る舞い、そのスコープは宣言された場所からパターンラベルに続く文の最後までです。
int v = /* ... */; inspect (v) { x => { std::cout << x; } // ^ identifier pattern };
ノート:もし識別子パターンがトップレベルで使われた場合、 goto
のラベルと同じ構文を持ちます。2
Expression Pattern
式パターンは以下の形式を持ち、
constant-expression
constant-expression
を e
としたとき、メンバ関数呼び出し e.match(v)
または ADL のみによる非メンバ関数呼び出し match(e, v)
が bool
に文脈的に変換可能かつ true
に評価されるような値 v
にマッチします。
match(x, y)
のデフォルトの挙動は x == y
です。
int v = /* ... */ ; inspect (v) { 0 => { std::cout << "got zero"; } 1 => { std::cout << "got one"; } // ˆ expression pattern };
enum class Color { Red, Green, Blue }; Color color = /* ... */ ; inspect (color) { Color::Red => // ... Color::Green => // ... Color::Blue => // ... // ˆˆˆˆˆˆˆˆˆˆˆ expression pattern };
ノート : デフォルトでは、 identifier は Identifier Pattern です。Case Pattern を参照してください。
static constexpr int zero = 0, one = 1; int v = 42; inspect (v) { zero => { std::cout << zero; } // ˆˆˆˆ identifier pattern }; // 出力 : 42
[WIP] Compound Patterns
Structured Binding Pattern
構造化束縛パターンは以下の二つの形式を持ちます。
[ pattern0, pattern1, . . . , patternN ]
[ designator0 : pattern0, designator1 : pattern1, . . . , designatorN : patternN ]
一つ目の形式は、それぞれの patterni
が v
の i 番目の要素にマッチするような値 v
にマッチします。ここで v
の要素とは、 それぞれの __ei
を説明専用の識別子としたとき auto&& [__e0, __e1, ... , __eN] = v;
のような構造化束縛宣言によって与えられるものです。
std::pair<int, int> p = /* ... */ ; inspect (p) { [0, 0] => { std::cout << "on origin"; } [0, y] => { std::cout << "on y-axis"; } // ˆ identifier pattern [x, 0] => { std::cout << "on x-axis"; } // ˆ expression pattern [x, y] => { std::cout << x << ',' << y; } // ˆˆˆˆˆˆ structured binding pattern };
二つ目の形式は、それぞれの patterni
が designatori
で指定された identifier
の名前を持つ v
の直接の非静的メンバにマッチするような値 v
にマッチします。
struct Player { std::string name; int hitpoints; int coins; }; void get_hint(const Player& p) { inspect (p) { [.hitpoints: 1] => { std::cout << "You're almost destroyed. Give up!\n"; } [.hitpoints: 10, .coins: 10] => { std::cout << "I need the hints from you!\n"; } [.coins: 10] => { std::cout << "Get more hitpoints!\n"; } [.hitpoints: 10] => { std::cout << "Get more ammo!\n"; } [.name: n] => { if (n != "The Bruce Dickenson") { std::cout << "Get more hitpoints and ammo!\n"; } else { std::cout << "More cowbell!\n"; } } }; }
ノート : 指示付き初期化とは異なり、 designator の順番はクラスのメンバが宣言された順と同じである必要はありません。
[WIP] Alternative Pattern
選択肢パターンは以下の形式を持ちます。
< auto > pattern
< concept > pattern
< type > pattern
< constant-expression > pattern
ここで、 v
をマッチされようとしている値、 V
を std::remove_cvref_t<decltype(v)>
、 Alt
を山括弧の内部にあるエンティティとします。
ケース1 : std::variant-like
選択肢パターンは、 std::variant_size_v<V>
が well-formed かつ整数に評価され、またAlt
が v
の現在のインデックスと互換であり pattern
が v
のアクティブな選択肢にマッチするとき、そのような v
にマッチします。
v
の現在のインデックスとは、メンバ関数呼び出し v.index()
または ADL のみの非メンバ関数呼び出し index(v)
によって与えられるもので、これを I
とします。v
のアクティブな選択肢とは、メンバ関数呼び出し v.get<I>()
または ADL のみの非メンバ関数呼び出し get<I>(v)
によって初期化された std::variant_alternative_t<I, V>&
のことです。
以下の四つのいずれかを満たすとき、 Alt
は I
と互換です。
Alt
がauto
である
Before | After |
---|---|
std::visit( [&](auto&& x) { strm << "got auto: " << x; }, v); |
inspect (v) { <auto> x => { strm << "got auto: " << x; } }; |
Alt
がconcept
であり、std::variant_alternative_t<I, V>
がそのconcept
を満たす
Before | After |
---|---|
std::visit([&](auto&& x) { using X = std::remove_cvref_t<decltype(x)>; if constexpr (C1<X>()) { strm << "got C1: " << x; } else if constexpr (C2<X>()) { strm << "got C2: " << x; } }, v); |
inspect (v) { <C1> c1 => { strm << "got C1: " << c1; } <C2> c2 => { strm << "got C2: " << c2; } }; |
Alt
がtype
でありstd::is_same_v<Alt, std::variant_alternative_t<I, V>>
がtrue
である
Before | After |
---|---|
std::visit([&](auto&& x) { using X = std::remove_cvref_t<decltype(x)>; if constexpr (std::is_same_v<int, X>) { strm << "got int: " << x; } else if constexpr (std::is_same_v<float, X>) { strm << "got float: " << x; } }, v); |
inspect (v) { <int> i => { strm << "got int: " << i; } <float> f => { strm << "got float: " << f; } }; |
std::variant<int, int> v = /* ... */ ; std::visit( [&](int x) { strm << "got int: " << x; }, v); |
std::variant<int, int> v = /* ... */ ; inspect (v) { <int> x => { strm << "got int: " << x; } }; |
Alt
がconstant-expression
であり、それがswitch
で使用可能かつI
と同じ値である
Before | After |
---|---|
std::variant<int, int> v = /* ... */ ; std::visit([&](auto&& x) { switch (v.index()) { case 0: { strm << "got first: " << x; break; } case 1: { strm << "got second: " << x; break; } } }, v); |
std::variant<int, int> v = /* ... */ ; inspect (v) { <0> x => { strm << "got first: " << x; } <1> x => { strm << "got second: " << x; } }; |
ケース2 : std::any-like
< type > pattern
Alt
が type
で、ADL のみの非静的メンバ関数呼び出し any_cast<Alt>(&v)
が有効なものであった場合にその結果を p
とすると、選択肢パターンは p
が bool
に文脈的に変換可能で かつ true
に評価され、 pattern
が *p
にマッチするとき、マッチします。
Before | After |
---|---|
std::any a = 42; if (int* i = any_cast<int>(&a)) { std::cout << "got int: " << *i; } else if (float* f = any_cast<float>(&a)) { std::cout << "got float: " << *f; } |
std::any a = 42; inspect (a) { <int> i => { std::cout << "got int: " << i; } <float> f => { std::cout << "got float: " << f; } }; |
[WIP] ケース3 : 多層的な型
< type > pattern
Alt
が type
で、std::is_polymorphic_v<V>
が true であるとき、 decltype(&v)
と同じcv修飾子を持つ Alt'
に対して dynamic_cast<Alt'*>(&v)
を p
とすると、選択肢パターンは p
が bool
に文脈的に変換可能で かつ true
に評価され、 pattern
が *p
にマッチするとき、マッチします。