SRELLの非互換の変更一覧

目次

非互換の変更

basic_regexの独自拡張

 SRELL 4.009~4.056までbasic_regexクラスには次のメンバ函数が独自拡張として存在していました。

replace() const

 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_resultstypedefには、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のテンプレート引数が推論できないためです。

補註
  • 4.012までは、*cmatch系を渡してほしい場合専用のcreplace()と、*smatch系を渡してほしい場合専用のsreplace()とがありましたが、煩雑化してしまったreplace()系を整理するため4.013で廃止しました。
  • 4.010までは 1) カスタムしたstd::basic_string<charT, ST, SA>型そのものと、2) コールバック函数に渡してほしいmatch_results<RandomAccessIterator, Alloc>との2つを、この順番で明示的に指定する必要がありました。

str_clip()

 これは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() const

 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;
}
			

アルゴリズム (regex_search)

 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についても同じです。

match_lblim_availフラグとmatch_results.lookbehind_limitメンバ

 SRELL 2.300~2.500には次のような独自拡張がありました。

 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::regexsrell::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」を実装した上でつなぎとして実装したものを削除し、今に至っています。

u8-prefixとu8c-prefix

 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-に変更しました。

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として定義されるようになっています。

u8-とu8c-
PrefixSRELL 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 が発生しています。