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:


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


My major concerns are


To minimize the number of the functions need to be overloaded.
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.)
How string_view in C++17 can help in this example. My version seems not take full advantages of it.
How to design a better function signature of the master template to relieve the pain when writing overloading?


Below is my first attempt.

#include &lt;iostream&gt;
#include &lt;list&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;
#include &lt;vector&gt;

template &lt;typename CharT, typename ContainerT&gt;
void split(
    std::basic_string_view&lt;CharT&gt; str,
    std::basic_string_view&lt;CharT&gt; delimiters,
    ContainerT &amp;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&lt;CharT&gt;::npos) {
        if (end - start) {
            conts.emplace_back(str, start, end - start);
        }
        start = end + len;
    }

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

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

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

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

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

int main(int argc, char **argv) {
    std::string str("haha,ha,,haha,,ha,,,ha,");
    std::vector&lt;std::string&gt; 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 &amp;cont
const CharT *delimiter


The style in C++ is to put the * or &amp; 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&lt;CharT&gt;::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&lt;CharT&gt; 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&lt;CharT&gt;


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 &lt;typename CharT, typename ContainerT&gt;
void split ( ⋯, ContainerT&amp; 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 &lt;typename CharT, typename ContainerT = vector&lt;basic_string&lt;CharT&gt;&gt;
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 &lt;typename Iter&gt;
void print(Iter begin, Iter end) {
    for (auto it = begin; it != end; ++it) {
        std::cout &lt;&lt; *it &lt;&lt; "/";
    }
    std::cout &lt;&lt; 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 &lt;typename R&gt;
void print(const R&amp; rng) {
    for (const auto&amp; item : rng) {
        std::cout &lt;&lt; item &lt;&lt; "/";
    }
    std::cout &lt;&lt; '\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!