SRELL 4.009~4.056までbasic_regex
クラスには次のメンバ函数が独自拡張として存在していました。
regex_replace()
は元の文字列には手を加えず、置換後の文字列は内部で新規に作ったものをreturnしてくるのに対して、このreplace()
は元の文字列中の正規表現にマッチした箇所を、直接別の文字列に書き換えます。
直接文字列を書き換える点以外は、ECMAScriptのString.prototype.split(RegExpオブジェクト, リミット)
に準拠した動作をします。
置換対象とできるのは、std::basic_string
型またはこれと同じメンバを持つクラスです(4.010まではstd::basic_string
型のみでした)。
置換後の文字列の指定方法としては「1) 書式文字列を渡す」「2) コールバック函数を渡す」の2通りがあります。
// charT はbasic_regexの第1テンプレート引数の型。 template <typename StringLike> void replace( StringLike &s, const charT *const fmt_begin, const charT *const fmt_end, const bool global = false) const; template <typename StringLike> void replace( StringLike &s, const charT *const fmt, const bool global = false) const; template <typename StringLike, typename FST, typename FSA> void replace( StringLike &s, const std::basic_string<charT, FST, FSA> &fmt, const bool global = false) const;
global
引数がfalse
の時は、最初に正規表現にマッチした部分だけが置換されます。true
の時は、対象文字列中の正規表現にマッチする箇所すべてが置換の対象となります。
書式文字列fmt
の書式はsrell::regex_replace()
のものと同じで、ECMAScript仕様書の Runtime Semantics: GetSubstitutionに準拠しています。
この表に掲載されている文字の並びは特殊な意味を持ち、それ以外のものはそのまま置換後の文字列になります。
// 書式指定による置換例。 #include <cstdio> #include <string> #include "srell.hpp" int main() { const srell::regex re("(\\d)(\\d)"); // 数字が2つ続いている箇所を探す。 std::string text("ab0123456789cd"); re.replace(text, "($2$1)", true); // $1と$2との順番を入れ替えて括弧でくくる。 std::printf("Result: %s\n", text.c_str()); return 0; } ---- 実行結果 ---- Result: ab(10)(32)(54)(76)(98)cd
replace()
は書式文字列の代わりにコールバック函数を引数として受け取ると、正規表現にマッチする箇所を見つける度にその函数を呼び出します。
// charT はbasic_regexの第1テンプレート引数の型。 template <typename StringLike, typename RandomAccessIterator, typename MA> void replace( StringLike &s, bool (*repfunc)( std::basic_string<charT, typename StringLike::traits_type, typename StringLike::allocator_type> &replacement_text, const match_results<RandomAccessIterator, MA> &m, void *), void *ptr = NULL) const; template <typename MatchResults, typename StringLike> void replace( StringLike &s, bool (*repfunc)( std::basic_string<charT, typename StringLike::traits_type, typename StringLike::allocator_type> &replacement_text, const MatchResults &m, void *), void *ptr = NULL) const;
コールバック函数 (repfunc
) のシグネチュアは次の通りです。
// charT はbasic_regexの第1テンプレート引数の型。 bool replacement_function( std::basic_string<charT> &replacemen_text, const match_results<const charT *> &m, // *cmatch用。 void *); bool replacement_function( std::basic_string<charT> &replacemen_text, const match_results<typename std::basic_string<charT>::const_iterator> &m, // *smatch用。 void *);
マッチした箇所の情報はmatch_results
型インスタンスに詰め込まれ、コールバック函数には第2引数として渡されてきます。
match_results
のtypedef
には、const charT *
ベースの*cmatch
系と、std::basic_string<charT>::const_iterator
ベースの*smatch
系とがありますので、コールバック函数のシグネチュアも2種類あります。
第3引数は、replace()
函数の第3引数がそのまま渡されてきます。コールバック函数に何か渡したい時に使います。
コールバック函数側では、置換後の文字列を第1引数に詰めてからreturn
します。戻り値としてtrue
を返すと、引き続き正規表現にマッチする箇所が見つかるたびにコールバック函数が呼ばれ、false
を返すとそれ以降コールバック函数は呼ばれなくなります。
// コールバック函数による置換例。 // %エスケープされた文字列のデコード。 #include <cstdio> #include <string> #include "srell.hpp" bool repfunc(std::string &out, const srell::u8cmatch &m, void *) { out.push_back(std::strtoul(m[1].str().c_str(), NULL, 16)); return true; } int main() { const srell::regex re("%([0-9A-Fa-f]{2})"); std::string c14("%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A"); std::string c9803(c14), c11(c14); re.replace(c9803, repfunc); // C++98/03 re.template replace<srell::smatch>(c11, [](std::string &out, const srell::smatch &m, void *) -> bool { // C++11 out.push_back(std::strtoul(m[1].str().c_str(), NULL, 16)); return true; }); re.template replace<srell::smatch>(c14, [](auto &out, const auto &m, auto) -> bool { // C++14以降 out.push_back(std::strtoul(m[1].str().c_str(), NULL, 16)); return true; }); std::printf("Result(C++98/03): %s\n", c9803.c_str()); std::printf("Result(C++11): %s\n", c11.c_str()); std::printf("Result(C++14-): %s\n", c14.c_str()); return 0; } ---- 実行結果 ---- Result(C++98/03): あいうえお Result(C++11): あいうえお Result(C++14-): あいうえお
コールバック函数へのポインタの代わりにラムダを使用する時は、ラムダの第2引数として受け取りたいmatch_results<RandomAccessIterator, Alloc>
の型をreplace()
のテンプレート実引数として明示的に指定する必要があります。match_results
のテンプレート引数が推論できないためです。
creplace()
と、*smatch系を渡してほしい場合専用のsreplace()
とがありましたが、煩雑化してしまったreplace()
系を整理するため4.013で廃止しました。std::basic_string<charT, ST, SA>
型そのものと、2) コールバック函数に渡してほしいmatch_results<RandomAccessIterator, Alloc>
との2つを、この順番で明示的に指定する必要がありました。
これはbasic_regex
のメンバではなくnamespace srell
の直下にあるクラスですが、replace()
で使うことを意図したものですのでここでご紹介します。
SRELL 4.011以降、str_clip()
というテンプレートクラスが追加されています。これはreplace()
で検索、置換する範囲を制限するためのユーティリティーです。
// str_clip()の使用例。 #include <cstdio> #include <string> #include "srell.hpp" int main() { const srell::regex re("."); std::string text("0123456789ABCDEF"); srell::str_clip<std::string> ctext(text); // テンプレート実引数には、割り当てるbasic_string型インスタンスtextの型 (std::string) を指定。 // pos & countペアでクリップ。4文字目から6文字分。 re.replace(ctext.clip(4, 6), "x", true); std::printf("By pos&count: %s\n", text.c_str()); // "0123xxxxxxABCDEF" // イテレータペアでクリップ。 re.replace(ctext.clip(text.begin() + 6, text.end() - 6), "y", true); std::printf("By iterators: %s\n", text.c_str()); // "0123xxyyyyABCDEF" re.template replace<srell::smatch>(ctext.clip(6, 2), [](std::string &out, const srell::cmatch &, void *) { out = "Zz"; return true; }); std::printf("By lambda: %s\n", text.c_str()); // "0123xxZzZzyyABCDEF" return 0; }
split()
は、検索対象文字列中の「正規表現とマッチする箇所」の前後を分割してゆき、その位置情報を収めたsub_match
型インスタンスを、第1引数として渡された配列コンテナの参照にpushしてゆきます。
次の点を除いて、ECMAScriptのString.prototype.split(RegExpオブジェクト, リミット)
の仕様に準じた動作をします。
limit
が指定されている場合、limit-1
回まではECMAScriptの仕様書通りに振る舞い、残り1回となったら未処理の文字列をまとめてpushする。
split()
の分割個数の上限を指定する機能というのはよく見られるものですが、JavaScriptのそれは少し変わっていて、分割をlimit
回行うとそれ以降のまだ調べていない部分の文字列はそのまま捨ててしまいます。この挙動は個人的にあまり嬉しくありませんので、上のような変更を加えました。
template <typename container, typename ST, typename SA> void split( container &c, const std::basic_string<charT, ST, SA> &s, const std::size_t limit = static_cast<std::size_t>(-1)) const; // 以下2つは4.011で追加。 template <typename container, typename BidirectionalIterator> void split( container &c, const BidirectionalIterator begin, // container::value_type::iteratorと同型もしくはキャスト可能。 const BidirectionalIterator end, const std::size_t limit = static_cast<std::size_t>(-1)) const; template <typename container> void split( container &c, const charT *const str, const std::size_t limit = static_cast<std::size_t>(-1)) const;
結果を受け取るコンテナ型c
は、メンバ函数としてpush_back()
が実装されていれば何でも使えます。
正規表現の中にキャプチャ括弧があった場合はそれらによって捕獲された文字列もpushされます。何も捕獲していない括弧のところも飛ばされず、空文字列がpushされます。
#include <cstdio> #include <string> #include <vector> #include "srell.hpp" template <typename Container> void print(const Container &c) { for (typename Container::size_type i = 0; i < c.size(); ++i) std::printf("%s\"%s\"", i == 0 ? "{ " : ", ", c[i].str().c_str()); std::puts(" }"); } int main() { std::string text("01:23:45"); srell::regex re(":"); std::vector<srell::csub_match> res; // またはsrell::ssub_match. re.split(res, text); // 無制限分割。 print(res); // { "01", "23", "45" } res.clear(); // split()内ではclear()されないので注意。 re.split(res, text, 2); // 2分割。 print(res); // { "01", "23:45" } // JavaScriptの場合 { "01", "23" } になる。 re.assign("(?<=(\\d?)):(?=(\\d?))"); // ':'の前後の文字を捕獲。 res.clear(); re.split(res, text); print(res); // { "01", "1", "2", "23", "3", "4", "45" } text.assign("caf\xC3\xA9"); // "café" re.assign(""); res.clear(); re.split(res, text); // char型の1文字単位で分割。 print(res); // { "c", "a", "f", "\xC3", "\xA9" } srell::u8cregex u8re(""); res.clear(); u8re.split(res, text); // UTF-8の1文字単位で分割。 print(res); // { "c", "a", "f", "é" } return 0; }
SRELL 2.600~4.064には独自拡張として次のようなオーヴァーロードがありました。
template <class BidirectionalIterator, class charT, class traits> bool regex_search( BidirectionalIterator first, BidirectionalIterator last, BidirectionalIterator lookbehind_limit, const basic_regex<charT, traits> &e, const regex_constants::match_flag_type flags = regex_constants::match_default);
引数としてmatch_results
を取らぬmatchやsearchは私自身使うことが皆無のため、API簡素化のため削除しました。
同じ3イテレータ方式でもmatch_results
を引数として取るほうは削除する予定はありません。またstd::regex
互換のAPIについても同じです。
SRELL 2.300~2.500には次のような独自拡張がありました。
match_results
: BidirectionalIterator lookbehind_limit
(Lookbehindの逆行限界指定)match_flag_type
: match_lblim_avail
(アルゴリズム函数に対して前記match_results.lookbehind_limit
が有効であることを伝えるためのフラグオプション)
match_lblim_avail
フラグがセットされると戻り読み (lookbehind) が行われる際、アルゴリズム函数に渡したmatch_results
型インスタンスのlookbehind_limit
メンバの値を「逆行できる限界」と見なすようになります。
const char text[] = "0123456789abcdefghijklmnopqrstuvwxyz"; const char* const begin = text; const char* const end = text + std::strlen(text); const char* const first = text + 10; // 'a' の位置に合わせる。 const srell::regex re("(?<=^\\d+)."); srell::cmatch match; match.lookbehind_limit = begin; std::printf("matched %d\n", srell::regex_search(first, end, match, re)); // 戻り読みも [first, end) の範囲でのみ行われるのでマッチしない。 std::printf("matched %d\n", srell::regex_search(first, end, match, re, srell::regex_constants::match_lblim_avail)); // match.lookbehind_limitまで逆行できるのでマッチする。 // 即ち match_lblim_avail指定時は [match.lookbehind_limit, end) というシークウェンスに対して // firstよりsearchを始める。
上の例にもあります通りmatch_lblim_avail
が指定された時は、^
は first
ではなく match.lookbehind_limit
にマッチするようになります。
SRELL 2.600以降では、regex_search()の引数としてlookbehindの逆行限界位置を指定できるようにしました。これに伴い上記の方法は廃止しました。
この2.600で導入した「3イテレータ方式」を最初から採用しなかったのは、引数の渡し方に次の2通りが考えられたためです。
// 渡し方1 bool regex_search( BidirectionalIterator first, BidirectionalIterator last, BidirectionalIterator lookbehind_limit, match_results<BidirectionalIterator, Allocator>& m, const basic_regex<charT, traits>& e, regex_constants::match_flag_type flags = regex_constants::match_default); // 渡し方2 bool regex_search( BidirectionalIterator lookbehind_limit, BidirectionalIterator first, BidirectionalIterator last, match_results<BidirectionalIterator, Allocator>& m, const basic_regex<charT, traits>& e, regex_constants::match_flag_type flags = regex_constants::match_default);
「渡し方1」はlookbehindの逆行限界を、通常の [first, last) への追加として渡すようにしたものです。「渡し方2」は、3つのiteratorを昇順に並べたものです。
これら2つは引数の型の並びがまったく同じですのでコンパイラには区別がつきません。そのため、例えばSRELLは渡し方1を採用したのに、将来C++規格のregex_search()
が渡し方2を使って拡張されたなどということが起こりますと、std::regex
とsrell::regex
との間に「コンパイル時にエラーとならない非互換」が発生するというかなり困った事態が発生してしまいます。
これを直すためにSRELL側で引数の並び順をC++規格に合わせるような変更を加えますと、今度は「SRELLを新しいものに更新したら挙動が変わってしまった」という、これまた困った問題が発生してしまいます。
折しもC++20でchar8_t
が導入されたことにより、templateの特殊化を利用すればUTF-8, UTF-16, UTF-32で処理をし分けるという方法が使えるようになったタイミングでもありましたので、C++委員会に対して<regex>
をUnicode対応にする提案を出してみて、委員会が<regex>
の拡張を現実的な選択肢として考えているのか明らかにしてみようと思い立ちました。
ただ当時のC++委員会は提案書を出しても長期間放置されることも珍しくなく、結果が出るまで逆行限界を指定する方法をなしのままにするというのもためらわれました。そこで「つなぎ」として実装したのが前記のmatch_results
に独自メンバを足すという方法です。お世辞にも綺麗な方法とは言えないので、C++規格が将来このような拡張をするとは考えにくく、この方法なら衝突の可能性がないと思われたためです。
結果、C++委員会が今後<regex>
に手を加える可能性はほぼゼロに近いと分かりましたので、SRELLのversion 2.600で「渡し方1」を実装した上でつなぎとして実装したものを削除し、今に至っています。
C++のversion問わずu8-というprefixは、u8"..."
文字列リテラルを直接扱えることを示しています。一方u8c-は、char
型の文字列をUTF-8として扱うことを示しています。
C++17までu8"..."
はconst char[N]
型でしたので、このような区別は必要ありませんでした。
しかしC++20でchar8_t
型が導入され、以後u8"..."
はconst char8_t[N]
型に変更されました。これによりSRELLでも新しい「char8_t
を使ったUTF-8文字列」と、従来からある「char
を使ったUTF-8文字列」とを区別する必要が出てきたため、UTF-8用のprefixも2種類必要になりました。
本来なら後から出来た「char8_t
を使ったUTF-8文字列」のほうに新しいprefixを用意すべきところなのでしょうが、既にstd::u8string
がそうであるように、今後u8-というprefixはC++規格のライブラリでもchar8_t
型に特殊化されたクラス、アルゴリズムという意味で使われるようになってゆくことが予想されます。
そこでC++標準ライブラリの命名規則との不整合を避けるため、SRELL 2.100以降では「char
型の文字列をUTF-8文字列として扱う」ことを意味するprefixをu8-からu8c-に変更しました。
basic_regex
: u8cregex
match_results
: u8ccmatch
, u8csmatch
sub_match
: u8ccsub_match
, u8cssub_match
regex_iterator
: u8ccregex_iterator
, u8csregex_iterator
regex_token_iterator
: u8ccregex_token_iterator
, u8csregex_token_iterator
一方u8-というprefixはSRELL 2.100以降、C++20以降の版に準拠してコンパイルされる場合はchar8_t
型と関連づけられ、C++17までの版に準拠してコンパイルされる場合は、u8c-というprefix付きクラスの単なるtypedef
として定義されるようになっています。
Prefix | SRELL 2.002まで | SRELL 2.100以降 | |
---|---|---|---|
C++17まで | C++20以降 | ||
u8- | char 型の文字列をUTF-8として扱う |
char8_t 型の文字列をUTF-8として扱う |
|
u8c- | (Prefixはまだ存在せず) | char 型の文字列をUTF-8として扱う |
SRELLのversion 1.nnnでは、ECMAScript 2017 (ES8) 規格書の21.2 RegExp (Regular Expression) Objects で定義されている正規表現に固定幅の戻り読み (lookbehind assertions) を加えたものが利用できました。
ただ次のような経緯により、SRELL 1.nnnとSRELL 2.000以降とでは戻り読みの挙動に違いがあります。
ECMAScript (JavaScript) の仕様を策定しているTC39はRegExpに追加する戻り読みについて、Perl5, Pythonなど多くのスクリプト言語が採用している固定幅限定の戻り読みではなく、制限のない可変幅の戻り読みを採用しました。
一見すると後者は前者の上位互換のように思われますが、実のところこれらは次のような場合に異なる結果をもたらします。
"abcd" =~ /(?<=(.){2})./
// 固定幅の戻り読みなら: $& = "c", $1 = "b".
// オートマトンは戻り読み内でも通常通り左から右へと文字列を照合してゆくため、
// "c" の直前にある "b" が「$1によって最後にキャプチャされたもの」となる。
// 可変幅の戻り読みなら: $& = "c", $1 = "a".
// オートマトンは戻り読み内では右から左へと走るため、"c" から2つ離れた
// "a"が「$1によって最後にキャプチャされたもの」となる。
SRELL 1が独自拡張として採用していたのは固定幅の戻り読みでした。対してSRELL 2.000以降には、RegExpの機能拡張に追随して可変幅の戻り読みが実装されています。このためSRELL 1.401 → SRELL 2.000では breaking change が発生しています。