Nice tidy code, and you've provided a test - that's good. You didn't mention warning options when compiling; I used g++-12 -std=c++17 -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Wconversion -Weffc++ -Wuseless-cast
and it was clean.
Interface
Taking input as a std::string_view
is perfect - no need to copy any character data. However, we forgot to include the <string_view>
header (<string>
isn't sufficient if we want portable code). We might want to generalise it to accept any character type, using template std::basic_string_view<CharT>
.
Consider returning a collection of views, and leave it to the caller to decide whether to construct string objects from these.
The return type of the function might be a constraint in some use cases - not everyone wants a std::vector
, and it might be hard for the allocator to find sufficient contiguous storage when the result is large. We could provide a version that accepts an output iterator - users could then use a std::back_insert_iterator
into any kind of collection, or even a std::ostream_iterator
for full streaming without using any storage.
Implementation
We're using the wrong npos
constant - we should be using std::string_view::npos
.
We don't actually need the pos
index, if instead we shorten the view by advancing its start point, so that we're always looking at the beginning of the string (note, this is cheap with a view, but very costly with a string object). We can use the remove_prefix()
member function:
while (!input_string.empty()) {
auto count = input_string.find_first_not_of(input_string.front());
if (count == input_string.npos) {
count = input_string.size();
}
grouping.emplace_back(input_string.substr(0, count));
input_string.remove_prefix(count);
}
This has the additional advantage that we don't need a separate path to handle empty input - that naturally happens with this loop.
As user Deduplicator observed, because the npos
value is very large, we can use std::min()
to restrict it to the string's size:
while (!input_string.empty()) {
auto count = std::min(input_string.size(),
input_string.find_first_not_of(input_string.front()));
*iter++ = input_string.substr(0, count);
input_string.remove_prefix(count);
}
Modified code
#include <algorithm>
#include <iterator>
#include <string_view>
#include <vector>
template<typename CharT,
typename Traits = std::char_traits<CharT>,
typename Iter>
auto
group_string_components(std::basic_string_view<CharT, Traits> input,
Iter iter)
{
while (!input.empty()) {
auto count = std::min(input.size(),
input.find_first_not_of(input.front()));
*iter++ = input.substr(0, count);
input.remove_prefix(count);
}
return iter;
}
template<typename CharT,
typename Traits = std::char_traits<CharT>>
auto
group_string_components(std::basic_string_view<CharT, Traits> input)
{
std::vector<std::basic_string_view<CharT, Traits>> grouping;
group_string_components(input, std::back_inserter(grouping));
return grouping;
}
If we had C++20, we could use constraints to restrict the Iter
type, and with C++23, we might use std::ranges::chunk_by_view
to split in this way.