[C++] <ranges>のviewを見る18 - split_view
split_view
split_view
は文字列分割に特化したlazy_split_view
です。
#include <ranges>
#include <iostream>
#include <string_view>
int main() {
// ホワイトスペースをデリミタとして文字列を切り出す
std::ranges::split_view sv{"split_view takes a view and a delimiter", ' '};
// split_viewは切り出した文字列のシーケンスとなる
// その要素は容易に文字列として扱う事ができる
// string_viewへの直接変換はC++23から
for (std::string_view str : sv) {
std::cout << str << '\n';
}
/*
split_view
takes
a
view
and
a
delimiter
*/
}
split_view
も分割した部分文字列を表すrange
を要素とするシーケンスを返します。しかし、lazy_split_view
と異なり内側のrange
は入力range
のイテレータを利用したstd::ranges::subrange
となるため、文字列であれば内側range
はcontiguous_range
かつcommon_range
となり、かなり扱いやすくなっています。
さらに、std::string_view
にはC++23からrange
コンストラクタが追加されているので、そのままストレートに変換する事ができます。C++20でも、for
の内部で変換する1行のコードを足すだけで切り出した部分文字列をstd::string_view
として扱うことができます。
#include <ranges>
#include <iostream>
#include <string_view>
int main() {
// ホワイトスペースをデリミタとして文字列を切り出す
std::ranges::split_view sv{"split_view takes a view and a delimiter", ' '};
for (auto inner_range : sv) {
// 内側rangeからstring_viewへの変換
std::string_view str{inner_range.begin(), inner_range.end()};
std::cout << str << '\n';
}
}
遅延評価
split_view
もまた遅延評価によって分割処理とview
の生成を行いますが、lazy_split_view
とは少し異なります。
split_view
では、.begin()
による外側イテレータの取得時に最初のデリミタ位置を探索し文字列先頭位置とともに記録しておきます。そして、外側イテレータのインクリメントのタイミングでデリミタ位置と文字列先頭位置の更新を行なっていきます。外側イテレータの間接参照では、現在の文字列先頭位置と次のデリミタ位置のイテレータから切り出す文字列をsubrange
によって返すため、内側range
は特別なことを何もしません。外側イテレータの終端チェックでは、デリミタ列に一致する文字列が終端に来ているケースを考慮するために少し判定が増えていますが、lazy_split_view
と比べると単純になっています。
std::ranges::split_view sv{"split_view takes a view and a delimiter", ' '};
// 構築時点では何もしない
auto outer_it = ranges::begin(sv);
// 外側イテレータの取得時、最初にデリミタ列に一致する部分範囲を計算
// 外側イテレータは、文字列先頭位置と次に出現するデリミタ位置をイテレータで管理している
// 現在の文字列先頭位置をp, 次に出現するデリミタ先頭位置をeとすると
// [p, e)の範囲が常に部分文字列に一致する
++outer_it;
// 外側イテレータのインクリメント時、文字列先頭位置pを前のデリミタ位置eで更新
// 次に出現するデリミタ列を探しeを更新
// [p, e)の範囲が次の部分文字列へ進む
// 内側rangeは"split_view"->"takes"へ進む
bool b = outer_it == ranges::end(sv); // false
// 外側range終端チェック
// 元のシーケンス上での終端チェックと
// 現在の位置でデリミタが出現しているかどうかを調べる
auto inner_range = *outer_it;
// 内側rangeの取得、subrangeを作って返す
// [p, e)の範囲をそのままsubrange{p, e}で返す
auto inner_it = ranges::begin(inner_range);
// 内側イテレータは元のrangeのイテレータを返す
++inner_it;
// 内側イテレータのインクリメントは、元の範囲のイテレータのインクリメント
char c = *inner_it; // a
// 元のシーケンスのイテレータのデリファレンスと等価
lazy_split_view
の様に過度な一般化と過度な遅延評価を行わない事によって、文字列分割時に結果を文字列に簡単に変換可能である様に実装されています。一方、begin()
の呼び出しで最初のデリミタ位置の探索を行っているため、range
コンセプトの意味論要件を満たすために常にキャッシュが使用されます。これによって、split_view
の外側range
はconst-iterable
でもborrowed_range
でもなくなります(とはいえそれはlazy_split_view
とほぼ同じことです)。
views::split
split_view
に対応するRangeアダプタオブジェクトがviews::split
です。
#include <ranges>
#include <iostream>
#include <string_view>
int main() {
const auto str = std::string_view("split_view takes a view and a delimiter");
for (auto inner_range : views::split(str, ' ')) {
for (char c : inner_range) {
std::cout << c;
}
std::cout << '\n';
}
// パイプラインスタイル
for (auto inner_range : str | views::split(' ')) {
for (char c : inner_range) {
std::cout << c;
}
std::cout << '\n';
}
}
views::split
はカスタマイゼーションポイントオブジェクトであり、引数の式r, p
によってviews::split(r, p)
のように呼び出されたとき、split_view(r, p)
を返します。
文字列による文字列の分割
文字列に特化したと言いつつ入力となるrange
はforward_range
であればよく、デリミタもforward_range
であればOKです。例えばstd::list
などを、あるいはそれによって分割する事ができるわけです。ただし、input_range
の分割をサポートしていません。
例えば、文字列を文字列で分割することができます。
#include <ranges>
#include <iostream>
#include <string_view>
using namespace std::literals;
int main() {
std::string_view input = "1, 12434, 5, 0000, 3942";
// カンマとホワイトスペースで区切りたい
// そのままだとナル文字\0が入るのでうまくいかない
for (std::string_view str : input | std::views::split(", ")) {
std::cout << str << '\n';
}
// 1, 12434, 5, 0000, 3942
std::cout << '\n';
// デリミタ文字列を2文字分の範囲にする
// string_viewsの示す範囲は\0を含めない
for (std::string_view str : input | std::views::split(", "sv)) {
std::cout << str << '\n';
}
/*
1
12434
5
0000
3942
*/
}
通常の文字列リテラルの示す範囲は文字列終端の\0
を含んでしまうため、文字列で文字列を分割する際は問題となります。例えばstd::string_view
を通すと文字列範囲として\0
を含まなくなるので意図通りになりますが、分かりづらいところなので結構罠です。
lazy_split_view
とsplit_view
C++20とC++20では当初、lazy_split_view
がsplit_view
という名前で追加されていました。当初のsplit_view
は現在のlazy_split_view
と同等のもので、とてもジェネリックな分割を担うものだったわけです。ところが、それは文字列分割には扱いづらかったため、P2210R2によってsplit_view
は文字列分割に特化され、当初のsplit_view
はlazy_split_view
として分離されました。
これはC++20規格完成後に行われており、P2210R2は欠陥報告(defect report : DR)としてC++20に遡って適用されました。そのため、C++20規格(N4861)にはこのsplit_view
の記載はないのですが、さも最初からそうであったかのように歴史修正されています。そして、range
ライブラリ周りではこの種の歴史修正(DR)が他にもたくさんあったりしています・・・
Discussion