C++ std::string 文字列の分割(split)|区切り文字/文字列に対応

C++の文字列型std::stringには、split関数やsplitメンバ関数が定義されていないため、手動で文字列分割を行う必要があります。

区切り文字の長さが2文字以上の場合には、文字列検索を活用した手作業による分割が必要になります。文字型(char)の値による分割を行う場合には、std::getline関数を活用したより簡潔な方法もあります。

目次

正規表現によるスプリット処理を行いたい場合は以下の記事を参考にしてください。

正規表現による文字列の分割

手動による文字列分割

文字列クラスのfindメンバ関数で区切り文字の出現位置を判定し、その位置を元にsubstrメンバ関数で文字列を切り出していく方法です。分割された各文字列はstd::vector型のリストに随時追加しています。

auto string    = std::string("a, b, c");    // 分割対象の文字列
auto separator = std::string(", ");         // 区切り文字
auto separator_length = separator.length(); // 区切り文字の長さ

auto list = std::vector<std::string>();

if (separator_length == 0) {
  list.push_back(string);
} else {
  auto offset = std::string::size_type(0);
  while (1) {
    auto pos = string.find(separator, offset);
    if (pos == std::string::npos) {
      list.push_back(string.substr(offset));
      break;
    }
    list.push_back(string.substr(offset, pos - offset));
    offset = pos + separator_length;
  }
}

list; // list == {"a", "b", "c"}

char型の文字や文字列ポインタによる分割を行いたい場合には以下の記述を利用します。

/** 文字型の場合(char) */
char separator = ',';
auto separator_length = std::string::size_type(1);

/** C言語スタイル文字列の場合(const char*) */
auto separator = ", ";
auto separator_length = std::char_traits<char>::length(separator);

findメンバ関数はオーバロードに対応しているため、上記の変数separatorseparator_lengthを先程のサンプルコードのものと置き換えるだけで動作します。

find_first_ofの利用に注意する

文字検索時にfindメンバ関数の代わりにfind_first_ofメンバ関数を用いる方法も知られていますが、文字型を受け取るfind_first_of(char)はfind(char)と同等の処理を行うため処理的な違いはなく、また処理速度の違いも発生しないことがほとんどです。

なお、find_first_ofに2文字以上の文字列を渡すこともできますが、この場合は文字集合による検索が行われることになるため注意が必要です。文字集合による検索時には、引数に渡した文字列に含まれるいずれかの文字が最初に出現した位置を返します。

std::string s = "A a, B b";
s.find(", ");          // 3(カンマと空白の並びが最初に出現した位置)
s.find_first_of(", "); // 1(空白が最初に出現した位置)

find_first_ofが用いられた区切り文字用の分割処理を、区切り文字列用に書き換える際には、findメンバ関数への書き換えが必要となります。できれば始めからfindを利用し、find_first_ofは使わないようにすることをオススメします。この場合の文字集合の仕様は思わぬ誤解やプログラムの不具合に繋がる恐れがあるためです。

std::getline関数を活用した文字列分割

std::getline関数は入力ストリームから行単位で文字列を読み込むための関数ですが、第三引数に区切り文字を指定することもできます。これを活用することで文字列分割の実現が可能となります。

// #include <sstream> // std::stringstream
// #include <istream> // std::getline

std::vector<std::string> v;

std::string s = ",a,b,,c,";
std::stringstream ss{s};
std::string buf;
while (std::getline(ss, buf, ',')) {
  v.push_back(buf);
}

v; // v == {"", "a", "b", "", "c"}

ただし、分割対象文字列の末尾が区切り文字だった場合には、意図した分割が行えなくなるため注意が必要です。多くのプログラミング言語では",a,".split(",")["", "a", ""]の結果を返しますが、std::getline関数による分割を行った場合には、["", "a"]という末尾空白が無視された結果が得られます。

この問題への対処としては、分割対象文字列の末尾文字を手動判定する方法が考えられます。

// v == {"", "a", "b", "", "c"}    , s == ",a,b,,c,"
if (!s.empty() && s.back() == ',') {
  v.emplace_back();
}
// v == {"", "a", "b", "", "c", ""}, s == ",a,b,,c,"

他にも分割対象文字列の末尾にダミーの区切り文字を追加するテクニックが考えられます。

std::stringstream ss{s + ','};
while (std::getline(ss, buf, ',')) {
  v.push_back(buf);
}
この場合、空の文字列に対する分割を行った場合には空文字を格納したリスト({""})が得られるようになるという副作用が発生します。もっともこの挙動はECMAScriptのString.prototype.splitメソッドと同等のものです。

ちなみに、前半で紹介したサンプル内で!buf.empty()による空文字チェックを行うようにすれば、",a,b,,c,"の分割結果を{"a", "b", "c"}という空文字を排除した形にすることもできます。

while (std::getline(ss, buf, ',')) {
  if (!buf.empty()) v.push_back(buf);
}
v; // v == {"a", "b", "c"}

split関数の自作方法

std::string型の文字列分割を実現するsplit関数の実装例です。戻り値は分割された文字列を要素とするstd::vector<std::string>型のリストです。基本的なロジックは先程解説した「# 手動による文字列分割」の物と同等ですが、若干処理効率が良く、また汎用的な関数となっています。

split("a-b", '-');              // {"a", "b"}
split("a-b", "-");              // {"a", "b"}
split("a-b", std::string{"-"}); // {"a", "b"}

split("a-b-", '-');         // {"a", "b", ""}
split("a--b", '-');         // {"a", "", "b"}
split("-a--b-", '-', true); // {"a", "b"}

split("ab", "");          // {"ab"}
split("ab", "", 0, true); // {"a", "b"}
split("", "");            // {""}
split("", "", 0, true);   // {}
split("", "", true, 0);   // {""}

第二引数には文字型や文字列型の区切り文字/区切り文字列を指定することが可能となっています。第三引数のignore_emptyは空文字の分割要素を除外するオプションです。第四引数のsplit_emptyは分割対象文字列と区切り文字列のいずれかが空文字だった際の挙動を制御するオプションです。split_emptyがtrueに指定された場合、分割対象文字列と区切り文字列の両者が空文字のケースでは空のリストを返します。区切り文字列のみが空文字のケースでは、分割対象文字列を一文字単位で分割します。なお、split_emptyはignore_emptyオプションの影響を受けません。

以下はstd::string限定の文字列分割関数です。

#include <string> // std::string, std::char_traits
#include <vector> // std::vector

template<class T> std::vector<std::string> split(const std::string& s, const T& separator, bool ignore_empty = 0, bool split_empty = 0) {
  struct {
    auto len(const std::string&             s) { return s.length(); }
    auto len(const std::string::value_type* p) { return p ? std::char_traits<std::string::value_type>::length(p) : 0; }
    auto len(const std::string::value_type  c) { return c == std::string::value_type() ? 0 : 1; /*return 1;*/ }
  } util;
  
  if (s.empty()) { /// empty string ///
    if (!split_empty || util.len(separator)) return {""};
    return {};
  }
  
  auto v = std::vector<std::string>();
  auto n = static_cast<std::string::size_type>(util.len(separator));
  if (n == 0) {    /// empty separator ///
    if (!split_empty) return {s};
    for (auto&& c : s) v.emplace_back(1, c);
    return v;
  }
  
  auto p = std::string::size_type(0);
  while (1) {      /// split with separator ///
    auto pos = s.find(separator, p);
    if (pos == std::string::npos) {
      if (ignore_empty && p - n + 1 == s.size()) break;
      v.emplace_back(s.begin() + p, s.end());
      break;
    }
    if (!ignore_empty || p != pos)
      v.emplace_back(s.begin() + p, s.begin() + pos);
    p = pos + n;
  }
  return v;
}

より汎用的な関数が必要な場合は、若干保守は面倒になりますが以下の実装を参考にしてみてください。配列型やポインタ型のNULL終端文字列に対する分割や、特定の要素型Tを持ったstd::basic_string<T>型に対する分割も可能となります。

assert( L'a' == split(L"a, b", std::wstring{L", "})[0].c_str()[0] );
assert( u'a' == split(std::u16string{u"a,b"}, u',')[0].c_str()[0] );
assert(  'b' == split(std::basic_string<unsigned short>{'a', ',', 'b'}, ',').back().c_str()[0] );
#include <string>      // std::string, std::char_traits
#include <vector>      // std::vector
#include <algorithm>   // std::search, std::for_each
#include <type_traits> // std::decay

//// 手動でchar以外の型を指定する場合(`split<char16_t>(u"a, b, c", u", ")`)
// template<class C = char, class S = std::basic_string<C>, class T, class U> std::vector<std::basic_string<C>> split(const T& s, const U& separator, bool ignore_empty = 0, bool split_empty = 0) {
//// std::string型とconst char*型の文字列のみで十分な場合
// template<class T, class U> std::vector<std::string> split(const T& s, const U& separator, bool ignore_empty = 0, bool split_empty = 0) {
//  using S = std::string;
//  using C = std::string::value_type;
template<class T> struct split_tmp_value_type_     { using type = typename T::value_type; };
template<class T> struct split_tmp_value_type_<T*> { using type = typename std::remove_const<T>::type; };
template<class T, class U, class S = std::basic_string<typename split_tmp_value_type_<typename std::decay<T>::type>::type>, class C = typename S::value_type>
std::vector<S> split(const T& s, const U& separator, bool ignore_empty = 0, bool split_empty = 0) {
  struct {
    auto  beg(const S& s) { return s.begin(); }
    auto  beg(const C* p) { return p;         }
    auto  end(const S& s) { return s.end();                                    }
    auto  end(const C* p) { return p ? p + std::char_traits<C>::length(p) : p; }
    auto& str(const S& s) { return s; }
    auto  str(const C* p) { return p; }
    auto  str(const C  c) { // return S{c}; // split("ab", '\0', 0, true) == {"ab"}
      struct {
        const C _a[2];
        operator const C*() { return _a; }
      } wrap_char{c, C()};
      return wrap_char;
    }
  } util;
  
  auto&& sep_ = util.str(separator);
  auto B = util.beg(sep_), E = util.end(sep_);
  auto b = util.beg(s)   , e = util.end(s);
  if (b == e) { /// empty string ///
    if (!split_empty || E != B) return {{}};
    return {};
  }
  
  auto v = std::vector<S>();
  if (B == E) { /// empty separator ///
    if (!split_empty) return {s};
    std::for_each(b, e, [&](auto&& c) { v.emplace_back(1, c); });
    return v;
  }
  
  auto n = static_cast<typename S::size_type>(E - B);
  while (1) {   /// split ///
    auto i = std::search(b, e, B, E);
    if (i == e) {
      if (ignore_empty && b - n + 1 == e) break;
      v.emplace_back(b, e);
      break;
    }
    if (!ignore_empty || b != i)
      v.emplace_back(b, i);
    b = i + n;
  }
  return v;
}
(2023/03/10 追記):上記の汎用版split関数に関して、「const修飾された変数」での利用ができないという指摘があったため、const対応を加えておきました。問題はsplit関数内部でコンパイルエラーの原因となる「std::basic_string<const char>」型が形成されてしまう点にあるため、「std::remove_const」にてconst修飾を取り除くよう対応するのが良いでしょう。修正内容は以下の通りです。
template<class T> struct split_tmp_value_type_<T*> { using type = T; };
↓
template<class T> struct split_tmp_value_type_<T*> { using type = typename std::remove_const<T>::type; };
広告