Windows と C++
最新の C++ で正規表現を使用する
Kenny Kerr
「C++ は、洗練された効率的な抽象化を開発して使用するための言語である」(Bjarne Stroustrup)
C++ の考案者によるこの発言は、この言語を私が気に入っている理由を端的に表しています。C++ では、用途に最適と言える言語機能やプログラミング スタイルを組み合わせて、問題に対する洗練された解決策を作り出すことができます。
C++11 では 1 つ 1 つがきわめて魅力的な機能が多数導入されましたが、ばらばらの機能の羅列しか知らなければ十分に使いこなすことはできません。このような機能の組み合わせこそ、C++ が多くの人に価値を認められた強力な言語へと成長した理由です。今回は、この点を例証するために、最新の C++ で正規表現を使用する方法を紹介します。C++11 標準では強力な正規表現ライブラリが導入されましたが、従来の C++ プログラミング スタイルのように単独でこのライブラリを使用すると、少々面倒な場合があります。残念ながら、ほとんどの C++11 ライブラリはしばしばこのような方法で導入されています。ただし、この方法にはそれなりの長所があります。新しいライブラリの簡潔な使用例を探している場合は、一度に多数の新しい言語機能を理解する必要に迫られるとかなり厄介なためです。それでも、C++ の言語機能やライブラリ機能の組み合わせこそ、C++ が生産性の高いプログラミング言語になっている秘訣です。
今回の例では、正規表現ではなく C++ に焦点を絞るために、当然ながらごく単純なパターンを使用します。なぜこれほど単純な問題に正規表現を使用するのだろうと思う方もいらっしゃるでしょうが、このようにすると、正規表現のしくみに悩まずに済みます。次に、簡単な例を示します。"Kenny Kerr" と "Kerr, Kenny" という形式の名前が混在している場合に、名前の文字列に照合する場合を考えてみましょう。名と姓を特定して、一貫した方法で表示する必要があります。まず、処理対象の文字列を定義します。
char const s[] = "Kerr, Kenny";
わかりやすくするために、今回は char 文字列だけを使用し、標準ライブラリの basic_string クラスについては、特定の一致結果を説明する場合を除いて使用しません。basic_string に問題はありませんが、個人的な経験では、正規表現を使用する処理のほとんどは、メモリ マップ ファイルを対象にしています。メモリ マップ ファイルの内容を文字列オブジェクトにコピーしても、アプリケーションの処理速度が低下するだけです。標準ライブラリの正規表現サポートでは、文字シーケンスをどのような方法で管理していてもまったく問題なく処理できます。
次に必要な項目は、一致オブジェクトです。
auto m = cmatch {};
このオブジェクトの正体は一致のコレクションです。cmatch は、char 文字列専用の match_results クラス テンプレートです。現時点では、一致の "コレクション" は空です。
ASSERT(m.empty());
また、結果を受け取る 2 つの文字列も必要です。
string name, family;
これで、regex_match 関数を呼び出せるようになりました。
if (regex_match(s, m, regex { R"((\w+) (\w+))" }))
{
}
regex_match 関数は、文字シーケンス全体に対するパターンの一致を試みます。これは、regex_search 関数とは対照的です。regex_search 関数の場合は、文字列内の任意の位置に一致があるかどうか検索します。今回は説明を簡潔にするために正規表現オブジェクトを "インライン" で作成しましたが、この手法には短所があります。この正規表現で繰り返し一致を探す場合は、正規表現オブジェクトを一度作成しておいてアプリケーションの有効期間中に使用し続ける方が、効率的なことがあります。先ほどのパターンは、"Kenny Kerr" 形式の名前に一致します。一致が見つかれば、部分文字列をコピーできます。
name = m[1].str();
family = m[2].str();
添字演算子は、指定した sub_match オブジェクトを返します。インデックス 0 は一致全体を表しますが、1 以上のインデックスは、正規表現で特定されたグループを指します。match_results オブジェクトと sub_match オブジェクトのいずれにも、部分文字列を作成したり割り当てたりする機能はありません。代わりに、一致やサブマッチの最初と最後を示すポインターと反復子で、文字の範囲を明確に指定します。これで、標準ライブラリで推奨されているおなじみの、上限を含まない範囲が設定されます。今回は各 sub_match オブジェクトで str メソッドを明示的に呼び出して、各サブマッチのコピーを文字列オブジェクトとして作成します。
これで 1 つ目の形式を処理できるようになりました。2 つ目の形式については、別のパターンを使用してもう一度 regex_match 関数 を呼び出す必要があります (厳密には両方の形式を 1 つの正規表現で照合することもできますが、今回の話題からそれるため説明しません)。
else if (regex_match(s, m, regex { R"((\w+), (\w+))" }))
{
name = m[2].str();
family = m[1].str();
}
このパターンは、"Kerr, Kenny" 形式の名前に一致します。インデックスを入れ替えていることに注意してください。これは、こちらの正規表現では最初のグループが姓を指し、2 つ目のグループが名を指すためです。以上で regex_match 関数の説明は終わりです。図 1 に、参考用のコード全体を示します。
図 1 regex_match 関数の参考例
char const s[] = "Kerr, Kenny";
auto m = cmatch {};
string name, family;
if (regex_match(s, m, regex { R"((\w+) (\w+))" }))
{
name = m[1].str();
family = m[2].str();
}
else if (regex_match(s, m, regex { R"((\w+), (\w+))" }))
{
name = m[2].str();
family = m[1].str();
}
else
{
printf("No match\n");
}
皆さんはどうかわかりませんが、私には図 1のコードが煩雑に思えます。正規表現ライブラリは確かに強力で柔軟ですが、それほど洗練されていません。match_results オブジェクトや sub_match オブジェクトの知識が必要なうえに、"コレクション" のインデックス方式や、結果の抽出方法を覚えておく必要もあります。コピーを作成しない手法もありますが、すぐに処理が煩わしくなります。
この記事では既に C++ の新しい言語機能を多数使用しており、中には皆さんにとって初耳の機能もあったかもしれませんが、いずれも特に驚くほどの機能ではなかったことと思います。ここからは、可変個引数テンプレートを使用して、正規表現の使用法に磨きを掛ける方法について説明します。抽象化を実用的で洗練されたままにするために、さらに多くの言語機能を投入して説明する代わりに、テキスト処理を簡略化する簡単な抽象化から説明しましょう。
最初に、必ずしも null で終了しない文字のシーケンスを表す、単純な型を作成します。これは以下のような strip クラスです。
struct strip
{
char const * first;
char const * last;
strip(char const * const begin,
char const * const end) :
first { begin },
last { end }
{}
strip() : strip { nullptr, nullptr } {}
};
再利用可能なクラスはもちろんいくつもありますが、簡単な抽象化を作成する際は依存関係の数を抑えた方が効果的だと考えました。
strip クラスにそれほど重大な機能はありませんが、一連の非メンバー関数を追加することにします。まず、一般的な範囲を定義する 2 つの関数を定義します。
auto begin(strip const & s) -> char const *
{
return s.first;
}
auto end(strip const & s) -> char const *
{
return s.last;
}
厳密には今回の例では必要ありませんが、この手法を使用すると、標準ライブラリのコンテナーやアルゴリズムとの互換性が大幅に向上します。これらの begin 関数と end 関数については、後でまた説明します。次は、make_strip ヘルパー関数を定義します。
template
auto make_strip(char const (&text)[Count]) -> strip
{
return strip { text, text + Count - 1 };
}
make_strip 関数は、文字列リテラルから strip オブジェクトを作成する場合に役立ちます。たとえば、次のように strip オブジェクトを初期化できます。
auto s = make_strip("Kerr, Kenny");
さらに、strip オブジェクトの長さやサイズを特定できると便利な場合がよくあります。
auto size(strip const & s) -> unsigned
{
return end(s) - begin(s);
}
ご覧のとおり、strip クラスのメンバーに対する依存関係を回避するために、ここでは begin 関数と end 関数を再利用しています。strip クラスのメンバーを保護してもよいでしょう。一方、アルゴリズム内でメンバーを直接操作できると便利な場合もよくありますが、密接な依存関係は必要な場合にのみ確立することにします。
もちろん、strip クラスから標準的な文字列を作成するのは簡単です。
auto to_string(strip const & s) -> string
{
return string { begin(s), end(s) };
}
元の文字シーケンスよりも有効期間が長い結果がある場合は、この処理が役立つときがあります。strip オブジェクトの基本操作についてはこれで完成です。strip オブジェクトを初期化して、サイズを特定できます。また、begin 関数と end 関数を使用すれば、範囲ベースの for ステートメントで文字を反復処理できます。
auto s = make_strip("Kenny Kerr");
for (auto c : s)
{
printf("%c\n", c);
}
最初に strip クラスを作成したとき、私はメンバー関数の "begin" と "end" を "first" と "last" の代わりに呼び出せるのではないかと考えていました。問題は、範囲ベースの for ステートメントがコンパイラで検出されると、関数として呼び出せる適切なメンバーの検索が試行されることでした。検索対象の範囲やシーケンスに begin と end というメンバーが含まれていない場合は、その外側のスコープで適切なメンバーのペアが検索されます。問題はここからです。begin と end というメンバーが見つかっても適切でなければ、それ以上詳しく調査されません。この動作は短絡的に思えるかもしれませんが、C++ の名前参照規則は複雑なので、これ以上少しでも綿密にすると、さらにわかりにくくて一貫性のない動作になります。
strip クラスはごく単純なコンストラクトですが、このクラス自体にはそれほど重大な機能はありません。それではこのクラスを正規表現ライブラリと組み合わせて、洗練された抽象化を実現しましょう。一致オブジェクトのしくみは正規表現処理の中でも煩雑な部分なので、隠すことをお勧めします。可変個引数テンプレートが役に立つのはこのような場合です。可変個引数テンプレートを理解するうえで重要なポイントは、最初の引数を残りの引数から分離できることです。引数を分離すると、通常はコンパイル時に再帰処理が発生します。今回の例では、一致オブジェクトを 2 つ目以降の引数に展開するように、可変個数テンプレートを定義できます。
template
auto unpack(cmatch const & m,
Args & ... args) -> void
{
unpack(m, args...);
}
"typename..." は、Args がテンプレート パラメーター パックであることを示しています。対応する "..." が args の型に含まれているので、args は関数パラメーター パックです。"sizeof..." という式は、パラメーター パックに含まれている要素の数を特定します。args の後にある最後の "..." は、コンパイラに対して、パラメーター パックを要素のシーケンスに展開するよう指示しています。
引数ごとに異なる型を指定できますが、今回の引数はどれも strip に対する非定数の参照です。可変個引数テンプレートを使用しているので、不定数の引数をサポートできます。unpack 関数の再帰的ではない部分はここまでです。続いて、次のような別の unpack 関数に、unpack 関数の引数をもう 1 つのテンプレート引数と共に渡します。
template
auto unpack(cmatch const & m,
strip & s,
Args & ... args) -> void
{
auto const & v = m[Total - sizeof...(Args)];
s = { v.first, v.second };
unpack(m, args...);
}
ただし、こちらの unpack 関数では一致オブジェクトの直後の引数を残りの引数から分離します。これがコンパイル時の再帰処理のしくみです。args パラメーター パックが空ではない場合は、残りの引数を渡しながら再度 unpack 関数を呼び出します。最終的に引数のシーケンスが空になったら、3 つ目の unpack 関数で次のように処理を終了する必要があります。
template
auto unpack(cmatch const &) -> void {}
この関数で行っている操作は特にありません。パラメーター パックが空になった可能性を認めているだけです。2 つ目の unpack 関数は、一致オブジェクトの展開の鍵となる情報を保持しています。最初の unpack 関数は、パラメーター パックにもともと含まれていた要素の数を取得します。このようなロジックが必要な理由は、再帰呼び出しを行うたびにサイズを減らしながら新しいパラメーター パックを作成するためです。元の合計サイズからパラメーター パックのサイズを引いている方法に注目してください。このような合計サイズや一定サイズがわかっていれば、一致のコレクションにインデックスを指定して、個別のサブマッチを取得したりサブマッチに対応する範囲を可変個の引数にコピーしたりすることができます。
これで一致オブジェクトの展開は完了です。必須の処理ではありませんが、一致のプレフィックスやサフィックスにアクセスするためだけに一致オブジェクトが必要な場合など、直接必要ではない一致オブジェクトを隠すと便利です。以上の機能をまとめて、一致の抽象化を簡略化しましょう。
template
auto match(strip const & s,
regex const & r,
Args & ... args) -> bool
{
auto m = cmatch {};
if (regex_match(begin(s), end(s), m, r))
{
unpack(m, args...);
}
return !m.empty();
}
この関数も可変個引数テンプレートですが、関数自体は再帰的ではありません。処理のために引数を元の unpack 関数に渡しているだけです。また、match ローカル オブジェクトを作成し、strip クラスの begin ヘルパー関数と end ヘルパー関数で検索シーケンスを定義しています。regex_match の代わりに regex_search を使用する関数も、ほぼ同じように作成できます。これで、図 1の例をずっと簡単な方法に書き直せるようになりました。
auto const s = make_strip("Kerr, Kenny");
strip name, family;
if (match(s, regex { R"((\w+) (\w+))" }, name, family) ||
match(s, regex { R"((\w+), (\w+))" }, family, name))
{
printf("Match!\n");
}
反復処理についてはどうでしょうか。unpack 関数は、反復検索の一致結果を処理する場合にも便利です。基本的な "Hello world" をさまざまな言語で表した、次のような文字列について考えてみましょう。
auto const s =
make_strip("Hello world/Hola mundo/Hallo wereld/Ciao mondo");
各言語の "Hello world" は、次の正規表現で照合できます。
auto const r = regex { R"((\w+) (\w+))" };
正規表現ライブラリには一致を反復処理する regex_iterator 関数がありますが、反復子を直接使用すると煩雑な場合があります。解決策の 1 つは、一致ごとに述語を呼び出す for_each 関数を作成することです。
template
auto for_each(strip const & s,
regex const & r,
F callback) -> void
{
for (auto i = cregex_iterator { begin(s), end(s), r };
i != cregex_iterator {};
++i)
{
callback(*i);
}
}
ラムダ式を使用してこの関数を呼び出せば、それぞれの一致を展開できます。
for_each(s, r, [] (cmatch const & m)
{
strip hello, world;
unpack(m, hello, world);
});
これは確かに機能しますが、このようなループ コンストラクトから簡単に抜け出せないことは、決まってストレスの種になります。範囲ベースの for ステートメントを使用すると、もっと便利な代替手法を利用できます。まず、範囲ベースの for ループの実装対象としてコンパイラに認識される簡単な反復子の範囲を定義します。
template
struct iterator_range
{
T first, last;
auto begin() const -> T { return first; }
auto end() const -> T { return last; }
};
このようにすると、iterator_range を返すだけの簡単な for_each 関数を作成できます。
auto for_each(strip const & s,
regex const & r) -> iterator_range
{
return
{
cregex_iterator { begin(s), end(s), r },
cregex_iterator {}
};
}
コンパイラによって反復処理が作成されるので、最小限の構文オーバーヘッドで範囲ベースの for ステートメントを記述でき、必要に応じて早めにループから抜け出せます。
for (auto const & m : for_each(s, r))
{
strip hello, world;
unpack(m, hello, world);
printf("'%.*s' '%.*s'\n",
size(hello), begin(hello),
size(world), begin(world));
}
コンソールには期待どおりの結果が表示されます。
'Hello' 'world'
'Hola' 'mundo'
'Hallo' 'wereld'
'Ciao' 'mondo'
C++11 や以降のバージョンを使用すると、洗練された効率的な抽象化を実現する最新のプログラミング スタイルを C++ ソフトウェア開発に導入できます。正規表現の文法には、非常に熟練した開発者でも鼻をくじかれることがあります。少し時間を割いて、洗練された抽象化を開発してみませんか。少なくとも、C++ での開発が楽しいものになります。
Kenny Kerrは、カナダを拠点とするコンピューター プログラマであり、Pluralsight の執筆者です。また、Microsoft MVP でもあります。彼のブログは kennykerr.ca(英語) で、Twitter は twitter.com/kennykerr(英語) でフォローできます。