可以使用 C++20 范围就地完成字符串的 trim 吗?

Can trim of a string be done inplace with C++20 ranges?

受 trim 的可爱 cppreference example 和 C++20 的启发,我编写了以下代码(我已将 return 类型更改为 void 和 arg std::string& 因为我的“问题”(我正在发明问题来学习 C++20)在使用 std::string_view arg 和 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);
}

这里的问题是这不是原地,这意味着创建了新字符串。 我可以编写更丑陋的代码来就地执行此操作,而且我知道传统的 C++ 算法不知道容器的存在,但我想知道 C++20 是否有一些技巧可以让我以优雅的方式执行 trim,但是也到位了。

这也是我的丑陋原地 trim(不确定它是否工作正常,但想法是它确实 trim 原地):

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

编辑:最初这个问题声称 in.assign 是 UB,但 T.C。纠正我。但根据我对 C++23 草稿分配的理解,仍会导致创建临时字符串。

是的,有可能:

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.

也许是这样的?

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));
}

assign 与任意迭代器需要创建一个临时的(因为改变字符串可以以任意方式影响迭代器的结果,并且因为抛出迭代器操作必须保持原始字符串不变),但是每个主要实现在给定字符串自己的迭代器时做正确的事情。

所以我们可以展开 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());

(不过我会亲自去听 Barry 的回答。)

此外,很少需要显式使用 views::all;适配器会自动执行。