split
June 29, 2026 ยท View on GitHub
A tiny, header-only string splitter for C++17.
What it is
split is a single-header utility that breaks a string into fields. It walks
the string once and yields each field lazily as a std::string_view into the
original buffer, so the iterator form allocates nothing. It also ships static
helpers that write fields straight into an output iterator when you'd rather
collect them into a container.
When to use it / when not
Use it when you want a small, dependency-free splitter that either streams fields without copying (the iterator) or fills a container in one call (the static helpers). It handles both "split on a full delimiter string" and "split on any character in a set."
Skip it if you need regex splitting, locale-aware tokenization, or a streaming parser over data that doesn't fit in a single contiguous string. For the zero-copy iterator you also have to keep the source string alive for as long as you read the views (see Notes & caveats).
Install
Header-only. Copy split.h somewhere on your include path and include it:
#include "split.h"
With CMake there's an INTERFACE target (split) you can link against. To pull
it in via FetchContent:
include(FetchContent)
FetchContent_Declare(
split
GIT_REPOSITORY https://github.com/Kronuz/split.git
GIT_TAG main
)
FetchContent_MakeAvailable(split)
target_link_libraries(your_target PRIVATE split)
The split target adds the include directory and requests cxx_std_17.
Usage
All examples assume #include "split.h".
Static helper into an output iterator
#include <iterator>
#include <string>
#include <vector>
std::vector<std::string> out;
Split<>::split(std::string("alpha,beta,gamma"), ',', std::back_inserter(out));
// out == { "alpha", "beta", "gamma" }
Lazy iterator, range-for
// Each field is a std::string_view into the original string.
Split<> sp(std::string("x/y/z"), '/');
for (const auto& field : sp) {
// use field ...
}
Full-delimiter mode vs. find-first-of mode
// Default Type::FIND splits on the whole delimiter string "::".
std::vector<std::string> a;
Split<>::split(std::string("a::b::c"), std::string("::"), std::back_inserter(a));
// a == { "a", "b", "c" }
// split_first_of splits on ANY character in the set, here ',' or ';'.
std::vector<std::string> b;
Split<>::split_first_of(std::string("a,b;c"), std::string(",;"), std::back_inserter(b));
// b == { "a", "b", "c" }
For a single-char delimiter both modes behave identically.
skip_blank
// skip_blank defaults to true, so empty fields are dropped.
std::vector<std::string> kept;
Split<>::split(std::string("a,,b,"), ',', std::back_inserter(kept));
// kept == { "a", "b" }
// Pass false to preserve empty fields.
std::vector<std::string> all;
Split<>::split(std::string("a,,b,"), ',', std::back_inserter(all), false);
// all == { "a", "", "b" } (no trailing empty; see Notes & caveats)
API reference
template <typename S = std::string, typename T = char>
class Split;
S is the string type that backs the splitter; T is the separator type
(a single char or a string of type S).
Constructors
Split() = default;
Split(String&& str, String&& sep, Type type = Type::FIND); // string delimiter
Split(String&& str, Sep&& sep, Type type = Type::FIND); // char delimiter
The two non-default constructors are SFINAE-constrained: the string-delimiter
overload requires both arguments to decay to the same type as S; the
char-delimiter overload requires the separator to decay to char. Both take the
source string by forwarding reference and store a copy in str.
Type modes
enum class Type : uint8_t {
FIND, // split on the full delimiter string (str.find)
FIND_FIRST_OF, // split on any char in the set (str.find_first_of)
SKIP_BLANK_FIND, // FIND, but keep blank fields
SKIP_BLANK_FIND_FIRST_OF, // FIND_FIRST_OF, but keep blank fields
};
Type picks the search strategy via a member-function-pointer
(dispatch_search). FIND matches the whole delimiter string;
FIND_FIRST_OF matches any single character in the separator set. Note that the
two SKIP_BLANK_* variants do not skip blanks; they set the internal
skip_blank flag to false, so blank fields are kept. The default constructor
state has skip_blank == true. For a single-character delimiter FIND and
FIND_FIRST_OF produce the same fields.
Iterators
iterator begin();
const_iterator begin() const; // also cbegin()
iterator end();
const_iterator end() const; // also cend()
iterator is an input iterator over std::string_view. Dereferencing returns a
view into the source string; incrementing advances to the next field. The
iterator also exposes last() (true on the final field) and an explicit operator bool() (false once exhausted). Split additionally offers empty(),
size() (distance from begin to end, which walks the string), and get_str().
Static helpers
template <typename OutputIt>
static void split(const S& str, const S& delimiter, OutputIt out, bool skip_blank = true);
template <typename OutputIt>
static void split(const S& str, char delimiter, OutputIt out, bool skip_blank = true);
template <typename OutputIt>
static void split_first_of(const S& str, const S& delimiter, OutputIt out, bool skip_blank = true);
template <typename OutputIt>
static void split_first_of(const S& str, char delimiter, OutputIt out, bool skip_blank = true);
split matches the whole delimiter; split_first_of matches any single
character in delimiter. Each writes S substrings (not views) into out,
which is any output iterator such as std::back_inserter(vec). skip_blank
defaults to true and drops empty fields. The char overloads treat the
delimiter as a single character; split_first_of(char) just forwards to
split(char) since a one-character set is the same as a one-character
delimiter.
Note an asymmetry between the helpers and the iterator: a trailing empty field
(the segment after a final delimiter) is never emitted by the static helpers
because they only emit the tail when prev < str.size(). The iterator handles
the trailing position through its own npos logic.
Build & test
Header-only, so there's nothing to compile for use. To run the smoke test:
c++ -std=c++17 -I. test/test.cc -o test/test && ./test/test
# or with CMake:
cmake -B build && cmake --build build && ctest --test-dir build
Requires a C++17 compiler.
Notes & caveats
- The lazy iterator yields
std::string_viewvalues that alias the original string. Keep the source alive for as long as you read those views. - The
Splitobject stores a copy of the source string instr, so the views are valid for the lifetime of theSplitobject, not the original argument. When you build aSplitfrom a temporary (as in the examples), bind it to a named variable before iterating so the copy outlives the loop. - The
SKIP_BLANK_FIND/SKIP_BLANK_FIND_FIRST_OFenum names are counterintuitive: they keep blank fields rather than skipping them. - The static helpers do not emit a trailing empty field after a final delimiter (see API reference).
Examples
examples/demo.cc is a runnable tour. A top-level CMake build
produces it next to the test:
cmake -B build && cmake --build build && ./build/split_demo
It iterates a Split with range-for so you can see each field is a
std::string_view into the source (no copies), fills a std::vector in one call
with the static helper, and contrasts the two delimiter modes on the same input:
split("a::b::c", "::") treats "::" as a whole delimiter (three fields), while
split_first_of("a::b::c", "::") treats it as a character set (a single :,
yielding the empty fields between the colons). It then splits on a real
multi-character set (, or ;), shows skip_blank dropping versus keeping empty
fields on "a,,b,", and uses the iterator's last() to join a path without a
trailing separator.
Provenance
Extracted from Xapiand.
License
MIT. Copyright (c) 2015-2019 Dubalu LLC. See LICENSE.