パターンマッチングの提案(P1371R3)を眺める(WIP)

※注意 : この記事はまだ執筆中です。タイトルの WIP が外れることを願っています。

個人的に気になったので、 P1371R3 : Pattern Matching についてメモ。
基本的に雑な和訳ですが、分かり易さのために改変したり注や例を挟むことがあります。


動機と領域

まず、既存の switchif などの条件分岐の構文では、単体の整数や真偽値による判定しかできず、標準ライブラリの提供する様々な型 (原文 : "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 式の丸括弧の中は switchif の丸括弧の中と基本的に等価ですが、変換も整数昇格も行われません。

筆者例

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 にもマッチします。この identifierv を参照する lvalue として振る舞い、そのスコープは宣言された場所からパターンラベルに続く文の最後までです。

int v = /* ... */;
inspect (v) {
    x => { std::cout << x; }
//  ^ identifier pattern
};

ノート:もし識別子パターンがトップレベルで使われた場合、 goto のラベルと同じ構文を持ちます。2

Expression Pattern

式パターンは以下の形式を持ち、

constant-expression

constant-expressione としたとき、メンバ関数呼び出し 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
};

ノート : デフォルトでは、 identifierIdentifier 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 ]

一つ目の形式は、それぞれの patternivi 番目の要素にマッチするような値 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
};

二つ目の形式は、それぞれの patternidesignatori で指定された 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 をマッチされようとしている値、 Vstd::remove_cvref_t<decltype(v)>Alt を山括弧の内部にあるエンティティとします。

ケース1 : std::variant-like

選択肢パターンは、 std::variant_size_v<V> が well-formed かつ整数に評価され、またAltv の現在のインデックスと互換であり patternv のアクティブな選択肢にマッチするとき、そのような v にマッチします。

v の現在のインデックスとは、メンバ関数呼び出し v.index() または ADL のみの非メンバ関数呼び出し index(v) によって与えられるもので、これを I とします。v のアクティブな選択肢とは、メンバ関数呼び出し v.get<I>() または ADL のみの非メンバ関数呼び出し get<I>(v) によって初期化された std::variant_alternative_t<I, V>& のことです。

以下の四つのいずれかを満たすとき、 AltI と互換です。

  • Altauto である
Before After
std::visit(
  [&](auto&& x) {
    strm << "got auto: " << x;
  },
v);
inspect (v) {
  <auto> x => {
    strm << "got auto: " << x;
  }
};
  • Altconcept であり、 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;
  }
};
  • Alttype であり 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;
  }
};
  • Altconstant-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

Alttype で、ADL のみの非静的メンバ関数呼び出し any_cast<Alt>(&v) が有効なものであった場合にその結果を p とすると、選択肢パターンは pbool に文脈的に変換可能で かつ 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

Alttype で、std::is_polymorphic_v<V> が true であるとき、 decltype(&v) と同じcv修飾子を持つ Alt' に対して dynamic_cast<Alt'*>(&v)p とすると、選択肢パターンは pbool に文脈的に変換可能で かつ true に評価され、 pattern*p にマッチするとき、マッチします。

[WIP] Parenthesized Pattern

[WIP] Case Pattern

[WIP] Dereference Pattern

[WIP] Extractor Pattern

[WIP] パターンガード


  1. 元々は expression form と statement form の二種類があったようですが、 R3 で統一を図ったと思われます (情報求む)

  2. ここは意味がよく掴めませんでした。inspect 式と goto との関係性についての議論なども行われていそうなので、後々仕様が固まるかもしれません。