I know you can ... do it manually by iterating each character ...
That is exactly would you would have to do, eg:
std::string str{"This i_s A stRIng"};
std::string::size_type pos = 0;
while (pos < str.size())
{
unsigned char ch = str[pos];
if (!::isalpha(ch))
{
if (!::isspace(ch))
{
str.erase(pos, 1);
continue;
}
}
else
{
str[pos] = (char) ::tolower(ch);
}
++pos;
}
Or:
std::string str{"This i_s A stRIng"};
auto iter = str.begin();
while (iter != str.end())
{
unsigned char ch = *iter;
if (!::isalpha(ch))
{
if (!::isspace(ch))
{
iter = str.erase(iter);
continue;
}
}
else
{
*iter = (char) ::tolower(ch);
}
++iter;
}
but is there a way to do something that would combine the std::transform and erase calls without having to run through the string multiple times?
You can use the standard std::accumulate() algorithm, as shown in francesco's answer. Although, that will not manipulate the std::string in-place, as the code above does. It will create a new std::string instead (and will do so on each iteration, for that matter).
Otherwise, you could use C++20 ranges, ie by combining std::views::filter() with std::views::transform(), eg (I'm not familiar with the <ranges> library, so this syntax might need tweaking):
#include <ranges>
auto alphaOrSpace = [](unsigned char ch){ return ::isalpha(ch) || ::isspace(ch); }
auto lowercase = [](unsigned char ch){ return ::tolower(ch); };
std::string str{"This i_s A stRIng"};
str = str | std::views::filter(alphaOrSpace) | std::views::transform(lowercase);
But, this would actually be a multi-pass solution, just coded into a single operation.