In real life, I use Boost’s split
, located in the string algos library.
Not in general that you should be familiar with std
and with a number of Boost libraries as ever-present common code.
Coming to C++ from most other languages, you should know that you shouldn’t be playing around with substr
. The string
class is a bit of an odd duck because it was being developed “conventionally” by standardizing existing practice and experience with other languages, and then all of a sudden STL comes along.
I was involved in implementing the string class from an early draft of the standards process around 1994, and it was even more conventional, using index positions and substrings everything.
Once STL was made the foundation of the Standard Library, the string
class was thrown out and a simple one made that’s similar to a vector but with handy support for string literals. That didn’t fly. The compromise was what we have today, which is a fully proper STL Container, and has some support for traditional string operations, thus allowing people to easily adopt it by changing out their home-made string class with minimal fuss, as opposed to having to completely rewrite the code to use STL algorithms.
for (size_t i = 0; i <= count; i++)
{
string x = (test.substr(token, test.find(Deli, token)-token));
parsed.push_back(x);
token += test.find(Deli, token +1) - (token-1);
test.find(Deli, token) != std::string::npos ? count++ : count;
}
Keeping the general idea you have of find the delimiter and then extract the range before that but after the previous one found, use iterators rather than string index positions.
So the starting place, rather than index 0
, is the begin
(or cbegin
) iterator. Your for
loop is structured oddly; it is not really a for
style loop at all so using the for
keyword is confusing; and you have to do the find
twice.
Keeping the same general idea, just express it cleaner in a way you will find common in C++ STL:
I’ll start with the easier case where the delimiter is a single character. So we have parameters (const string& test, const char delim)
Set things up:
using std::cbegin();
using std::cend(); // "two-step"; required for more generic code
auto start = cbegin(test);
auto End = cend(test); // so I don’t have to keep calling it
The idiom of using unqualified non-member forms for begin
etc. is preferred (ref 1,ref 2), and will make your code work with templates. (and make all code look the same rather than writing it one way if not using templates and another way in a template) Code often evolves by taking what you did once as a plain function and generalizing it somehow; or, the type of something in a large project is changed and you have to hunt down all uses and fix things, so the same techniques used when you don’t really know the exact type in the first place will help you here too!
Anyway, here is your starting point, a pointer to the beginning of the whole string as the start of the first token. Now you loop, finding the delimiter beyond this position, make that the End, extract what’s between, then update the start to resume where the End was on this iteration.
while ( ??? how do I know when I’, done ???) {
auto token_end = std::find (start, End, delim);
The find
algorithm will stop with the iterator pointing at the found character, or at End
. Either way is good for us! No special testing needed. Note that ranges are delimited as half-open: including the begin, excluding the end. That is, an end iterator points one past the last char to keep. That means everything works naturally without any adjustments or fiddling:
string token { start, token_end };
Note that a constructor takes a pair of iterators; exactly what we have! You can see now why substr
is not needed; you can just create a string with a range directly, not needing a special function call.
Now you want to collect the results. Keeping things (fairly) simple:
parsed.push_back(std::move(token));
Wrapping the argument in move
makes it more efficient. The whole thing about move semantics is another subject to learn.
Now to advance:
start = token_end + 1;
The next token starts just after the delim; we don’t want to look at that character again.
But here we see a boundary condition. If token_end
was the end of the string, this is an error. In fact, it signals the end of the loop! No more work needs to be done. So now I can go back and change the while
to an indefinite loop and write the test here.
for (;;) {
⋮
if (token_end == End) break;
start= token_end + 1;
}
and that should be it.
If the delimiter is a string of characters, not a single char, it is slightly more complex.
The suitable algorithm is search
, and that’s a trivial change. Checking the end and updating the iteration is a tad more complex, though.
vector<string> parse (const string& test, const string& sep)
{
vector<string> retval;
using std::cbegin();
using std::cend(); // "two-step"; required for more generic code
using std::length();
auto start = cbegin(test);
auto End = cend(test); // so I don’t have to keep calling it
for (;;) {
auto token_end = std::search (start, End, cbegin(sep), cend(sep));
retval.emplace_back(start,token_end);
if (token_end == End) break;
start= token_end + length(sep);
}
return retval;
}
Hmm, so it wasn’t any harder after all; just replace the +1 with the proper length of the separator. That’s a good sign that the algorithm was structured well to match the way iterators and the standard algorithms work.
Now… you notice that the only things you do with the parameters are getting the iterators into them. You do not rely on any members of std::string
at all. So, is would be perfect to use the rather new (C++17) string_view
here instead.
vector<string> parse (const string_view test, const string_view sep)
None of the code has to be changed, but now when you call it with a lexical string literal like: parse("this is a test", " ")
it does not have to construct a temporary std::string
object and copy the literal string into it. That’s the point of string_view
, since this is a common thing.
Good luck, and keep trying to get into C++ !