2
\$\begingroup\$

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;
}
\$\endgroup\$
1
\$\begingroup\$

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!

\$\endgroup\$
8
  • \$\begingroup\$ 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). \$\endgroup\$ – hoffmale May 27 '18 at 16:21
  • 2
    \$\begingroup\$ @hoffmale that is moot because you don’t put more than one declarator per statement. int* a= whatever; ⋯ int b = foo(a); \$\endgroup\$ – JDługosz May 27 '18 at 16:55
  • 1
    \$\begingroup\$ I still have my first edition C++ Programming Language from 1987. I’ll have to find the specific passages and post it somewhere. \$\endgroup\$ – JDługosz May 27 '18 at 16:57
  • \$\begingroup\$ 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. \$\endgroup\$ – stackunderflow May 28 '18 at 1:36
  • 1
    \$\begingroup\$ There is no while with an init-statement in the standard C++ yet. \$\endgroup\$ – Evg Jun 21 '20 at 6:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.