# Template function for splitting strings in C++

I'd like to write a function to split strings in C++ to check my template learning. I've also read some relavent questions in this site but most of them don't use templates or string_view. A related question in StackOverflow I asked is here.

My goal is to write a function such that:

1. The function can split std::basic_string, not only std::string.
2. The delimiter can be a CharT, or const CharT* p or std::basic_string<CharT>, multiple chars means the delimiter should be fully matched (like str.split in Python), not any of the chars.
3. The results are put into a sequence container from STL.
4. The user need not do any explicit conversion when parsing arguments, e.g. std::string_view(some_str).
5. (Optional) The function can split const CharT* p or any string literals.

My major concerns are

1. To minimize the number of the functions need to be overloaded.
2. To write template codes in a more modern C++ way. (For example, I've learn a lot by reading boost::algorithm::trim, many generic functions work on iterator ranges. But the boost version of split seems a bit complicate for me right now.)
3. How string_view in C++17 can help in this example. My version seems not take full advantages of it.
4. How to design a better function signature of the master template to relieve the pain when writing overloading?

Below is my first attempt.

#include <iostream>
#include <list>
#include <string>
#include <string_view>
#include <vector>

template <typename CharT, typename ContainerT>
void split(
std::basic_string_view<CharT> str,
std::basic_string_view<CharT> delimiters,
ContainerT &conts) {
conts.clear();
std::size_t start = 0, end;
std::size_t len = delimiters.size();
while ((end = str.find(delimiters, start)) !=
std::basic_string_view<CharT>::npos) {
if (end - start) {
conts.emplace_back(str, start, end - start);
}
start = end + len;
}

if (start != std::basic_string_view<CharT>::npos && start < str.size()) {
conts.emplace_back(str, start, str.size() - start);
}
}

template <typename CharT, typename ContainerT>
void split(
const std::basic_string<CharT> &str,
const std::basic_string<CharT> &delimiters,
ContainerT &conts) {
split(
std::basic_string_view<CharT>(str),
std::basic_string_view<CharT>(delimiters), conts);
}

template <typename CharT, typename ContainerT>
void split(
std::basic_string<CharT> str, const CharT *delimiter, ContainerT &conts) {
split(
std::basic_string_view<CharT>(str),
std::basic_string_view<CharT>(delimiter), conts);
}

template <typename CharT, typename ContainerT>
void split(
const std::basic_string<CharT> &str, CharT delimiter, ContainerT &conts) {
split(
std::basic_string_view<CharT>(str),
std::basic_string_view<CharT>(&delimiter, 1), conts);
}

template <typename Iter>
void print(Iter begin, Iter end) {
for (auto it = begin; it != end; ++it) {
std::cout << *it << "/";
}
std::cout << std::endl;
}

int main(int argc, char **argv) {
std::string str("haha,ha,,haha,,ha,,,ha,");
std::vector<std::string> strs;

split(std::string_view(str), std::string_view(","), strs);
print(strs.begin(), strs.end());

split(str, std::string(","), strs);
print(strs.begin(), strs.end());

split(str, ',', strs);
print(strs.begin(), strs.end());

split(str, ",,", strs);
print(strs.begin(), strs.end());

return 0;
}


Well, the formatting is peculiar — I can’t see where the function begins.

ContainerT &cont
const CharT *delimiter


The style in C++ is to put the * or & with the type, not the identifier. This is called out specifically near the beginning of Stroustrup’s first book, and is an intentional difference from C style.

std::size_t start = 0, end;
⋮
while ((end = str.find(delimiters, start)) !=
std::basic_string_view<CharT>::npos)


⧺ES.10: Declare one name (only) per declaration.

Meanwhile, you can declare end just where it is needed, not ahead of time.

while (size_t end=str.find(delimiters, start); end != str.npos)


You can also make the clauses of the condition more readable by making the names short: For example, see that I used the same instance str to get npos rather than its class name.

The function can split std::basic_string, not only std::string.
How string_view in C++17 can help in this example. My version seems not take full advantages of it.

std::basic_string_view<CharT> str,


Your input argument str will be able to take basic_string of any character type, but only using the default Traits. You might do that on purpose, if Traits are never used other than the defaults?

A string_view will take a string_view, string, or C-style lexical string.

The delimiter can be a CharT, or const CharT* p or std::basic_string<CharT>

So this first form will take the latter two types, as well as string_view. You do not need to overload to take CharT delimiter separately.

You do not need to overload to take basic_string for both parameters, because the string_view will take those!

So, two of the overloads should just be removed. The last one, that takes a CharT delimiter, needs to take the string_view for str so it will handle different types for that — you have it taking a basic_string only, which will be inefficient (copied!) if passed a const CharT* or string_view.

template <typename CharT, typename ContainerT>
void split ( ⋯, ContainerT& conts)


Don’t use “out” parameters. Return values (⧺F.20).

I can see the appeal here of getting template argument deduction on the return value, but that does not make up for the lack of composibility. You can add a default type to the ContainerT template parameter, which is handy for the case where the user doesn’t care.

template <typename CharT, typename ContainerT = vector<basic_string<CharT>>
ContainerT split ( ⋯ )


You are assuming that the container holds elements which have the form of constructor you are expecting.

Anyway, now your first example in main becomes:

auto strs = split (str, ",");


template <typename Iter>
void print(Iter begin, Iter end) {
for (auto it = begin; it != end; ++it) {
std::cout << *it << "/";
}
std::cout << std::endl;
}

print(strs.begin(), strs.end());


You should just take the container, not separate begin/end. That not only makes passing easier, but makes the function easier too!

template <typename R>
void print(const R& rng) {
for (const auto& item : rng) {
std::cout << item << "/";
}
std::cout << '\n';
}

print(strs);


Oh, and don’t use std::endl.

I also should point out that it’s good practice how you have unit tests for your function!

• Regarding your first point: While I get where you come from ("it is a type, it belongs together"), sadly, C++ declarations tend to advocate the other style for clarity. int *a, b; IMO expresses better that b is not of type int* than int* a, b;. (Yes, dedicated C++ programmers will know this tidbit, but it will come to surprise beginners or people switching between languages). – hoffmale May 27 '18 at 16:21
• @hoffmale that is moot because you don’t put more than one declarator per statement. int* a= whatever; ⋯ int b = foo(a); – JDługosz May 27 '18 at 16:55
• I still have my first edition C++ Programming Language from 1987. I’ll have to find the specific passages and post it somewhere. – JDługosz May 27 '18 at 16:57
• Hi, thanks for reading my code. I agree with the format issue of my code since it was formatted using a mixed version of clang-format setting. I don't even know that declaration can happen in a while condition. – stackunderflow May 28 '18 at 1:36
• There is no while with an init-statement in the standard C++ yet. – Evg Jun 21 '20 at 6:57