C++ 区切り文字/文字列による文字列の分割(split)std::string/文字列ポインタ

C++の文字列型にはsplitメンバ関数が存在しないため、手動で文字列分割を行う必要があります。

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

目次

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

正規表現による文字列の分割
スポンサーリンク

手動による文字列分割

文字列クラスのfindメンバ関数で区切り文字の出現位置を判定し、その位置を元にsubstrメンバ関数で文字列を切り出していく方法です。

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を先程のサンプルコードのものと置き換えるだけで動作します。

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型の文字列分割を実現する関数の実装例です。戻り値は分割された文字列を要素とする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 = T;                      };
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;
}

広告