4

Inspired by cute cppreference example of trim with C++20 I have written the following code(I have changed the return type to void and arg to std::string& since my "problem"(I am inventing problems to learn C++20) does not exist in original code that uses std::string_view arg and returns std::string).

void trim(std::string&  in)
{
    auto view
        = std::views::all(in)
        | std::views::drop_while(isspace)
        | std::views::reverse
        | std::views::drop_while(isspace)
        | std::views::reverse;
        std::string result{view.begin(), view.end()};
        in = std::move(result);
}

Issue here is that this is not inplace, meaning that new string is created. I can write uglier code that does this inplace and I know that traditionally C++ algorithms have no idea that containers exist, but I wonder if C++20 has some tricks that enable me to do the trim in elegant way, but also inplace.

Here is my ugly inplace trim also(not sure if it works ok, but idea is that it does the trimming inplace):

void trim2(std::string& s) {
    // ugly and error prone, but inplace
    const auto it1 = std::ranges::find_if_not(s, isspace);
    const auto it2 = std::ranges::find_if_not(s.rbegin(), s.rend(), isspace);
    const size_t shift = (it1==s.end()) ? 0: std::distance(s.begin(), it1);
    const size_t result_size = s.size() - shift - ((it2==s.rend()) ? 0 : std::distance(s.rbegin(), it2));
    std::shift_left(s.begin(), s.end(), shift);
    s.resize(result_size);
}

godbolt

edit: originally this question claimed in.assign would be UB, but T.C. corrected me. But based on my understanding of C++23 draft assign would still cause the temporary string to be created.

NoSenseEtAl
  • 28,205
  • 28
  • 128
  • 277
  • Working this out in my head: a straight-forward, in-place trim consists of: 1) a `find_if`, 2) an assignment, 3) a while loop with an if-conditioned assignment and 4) an erase(). A fairly basic, straightforward, simple implementation. Ranges are not a solution for every problem. The straightforward, simple approach, is done in exactly one iteration over the whole string. The absolute minimum that logic dictates is possible for something like this. How many times does the above go over, and over, and over, the range? – Sam Varshavchik Apr 01 '21 at 01:10
  • @SamVarshavchik probably twice(once for determining the view, and once for copy), since range views are maximally lazy. But I did not profile/debug this code, just a guess. – NoSenseEtAl Apr 01 '21 at 01:14
  • Where does cppreference say that for std::string? – T.C. Apr 01 '21 at 01:52
  • @T.C. my mistake, I clicked wrong link, it is for std::vector::assign – NoSenseEtAl Apr 01 '21 at 02:04
  • @T.C. ah wait... assign is safe, but slow... I just read the draft of C++23, they do make copies... Equivalent to:return assign(basic_string(first, last, get_allocator())); – NoSenseEtAl Apr 01 '21 at 02:15

3 Answers3

4

Perhaps something like this?

void trim(std::string& s) {
    auto not_space = [](unsigned char c){ return !std::isspace(c); };

    // erase the the spaces at the back first
    // so we don't have to do extra work
    s.erase(
        std::ranges::find_if(s | std::views::reverse, not_space).base(),
        s.end());

    // erase the spaces at the front
    s.erase(
        s.begin(),
        std::ranges::find_if(s, not_space));
}
Barry
  • 286,269
  • 29
  • 621
  • 977
3

assign with arbitrary iterators needs to create a temporary (because mutating the string can affect the iterator's result in arbitrary ways, and because a throwing iterator operation must leave the original string unmodified), but every major implementation does the right thing when given the string's own iterators.

So we can just unwrap the two level of reverse_iterator-ness:

auto view
    = in
    | std::views::drop_while(isspace)
    | std::views::reverse
    | std::views::drop_while(isspace)
    | std::views::reverse;

in.assign(view.begin().base().base(), view.end().base().base());

(I would have gone for Barry's answer myself though.)

Also, there's rarely any need to explicitly use views::all; the adaptors do it automatically.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • This is actually amazing. I will never use this since it is too obscure, but the fact this trick works is amazing. – NoSenseEtAl Apr 01 '21 at 03:05
  • 1
    @康桓瑋 `views::common` does nothing here - `reverse_view` is already common. The point of the `base()` calls is not to make the view common, it's to take advantage of implementation optimizations by unwrapping the iterators. – T.C. Apr 01 '21 at 19:26
0

Yes, it is possible:

void trim(std::string& s) {
  auto view = s
      | std::views::drop_while(isspace)
      | std::views::reverse
      | std::views::drop_while(isspace)
      | std::views::reverse;
  auto [in, out] = std::ranges::copy(view, s.begin());
  s.erase(out, s.end());
}

demo.

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • are you sure this is fine? Copies all elements in the range [first, last) starting from first and proceeding to last - 1. The behavior is undefined if result is within the range [first, last). In this case, ranges::copy_backward may be used instead. – NoSenseEtAl Apr 01 '21 at 02:41
  • I am mostly concerned about the fact that std::ranges::copy does not allow overlapping src and destination. I think if string is not trimmed at front your first it of source and first it of result are same – NoSenseEtAl Apr 01 '21 at 02:53