【问题标题】:Why must a std::ranges::filter_view object be non-const for querying its elements?为什么 std::ranges::filter_view 对象必须是非常量才能查询其元素?
【发布时间】:2021-08-12 11:28:50
【问题描述】:
#include <ranges>
#include <iostream>
#include <string_view>

using namespace std::literals;

int main()
{
    auto fn_is_l = [](auto const c) { return c == 'l'; };

    {
        auto v = "hello"sv | std::views::filter(fn_is_l);
        std::cout << *v.begin() << std::endl; // ok
    }

    {
        auto const v = "hello"sv | std::views::filter(fn_is_l);
        std::cout << *v.begin() << std::endl; // error
    }
}

见:https://godbolt.org/z/vovvT19a5

<source>:18:30: error: passing 'const std::ranges::filter_view<
                       std::basic_string_view<char>, main()::
                       <lambda(auto:15)> >' as 'this' argument discards
                       qualifiers [-fpermissive]
   18 |         std::cout << *v.begin() << std::endl; // error
      |                       ~~~~~~~^~
In file included from <source>:1:/include/c++/11.1.0/ranges:1307:7: 
     note: in call to 'constexpr std::ranges::filter_view<_Vp, 
           _Pred>::_Iterator std::ranges::filter_view<_Vp, Pred>
           ::begin() [with _Vp = std::basic_string_view<char>; _Pred =
           main()::<lambda(auto:15)>]'
 1307 |       begin()
      |       ^~~~~

为什么 std::ranges::filter_view 对象必须是非常量才能查询其元素?

【问题讨论】:

  • 你试过用cbegin()代替吗?
  • 默认情况下,std::ranges::view 对象没有 cbegin 成员。

标签: c++ constants standards c++20 std-ranges


【解决方案1】:

为了提供range 所需的摊销常数时间复杂度,filter_view::begin 将结果缓存在*this 中。这会修改*this 的内部状态,因此不能在const 成员函数中完成。

【讨论】:

  • 那很好奇有没有缓存的结果能保证是一样的。 (可以吗?)然后 mutable 是一种选择,对吗? const 是关于对象的外部 api 而不是内部状态。正确使用 mutable 关键字并非易事,但可以做到。
  • @bradgonesurfing 对,技术上这是可能的,但这需要线程同步(以满足const函数的要求),这被认为太昂贵了。
  • 不允许按线程进行缓存吗?
  • @dan04 - 不再满足时间复杂度要求,至少从迂腐的角度来看。
  • 仅供参考:P0789R3 1.3.1 “过滤器适配器不是 const-iterable”解释了 filter_view 的设计决策。
【解决方案2】:

这里的时间复杂度要求来自[range.filter.view]中对filter_view::begin()的描述:

constexpr iterator begin();

返回{*this, ranges​::​find_­if(base_­, ref(*pred_­))}.

备注:为了在filter_­view 模型时提供range 概念所需的摊销常数时间复杂度 forward_­range,此函数将结果缓存在 filter_­view 用于后续调用。

也就是说,实现需要在内部缓存ranges​::​find_if找到的满足谓词的迭代器,这使得后续对begin()的每次调用都可以简单的在常量时间内返回缓存的值,就​​像libstdc++一样确实:

template<input_range _Vp, indirect_unary_predicate<iterator_t<_Vp>> _Pred>
class filter_view : public view_interface<filter_view<_Vp, _Pred>> {
  _Vp _M_base = _Vp();
  __box<_Pred> _M_pred;
  _CachedPosition<_Vp> _M_cached_begin;

public:
  // ...
  constexpr _Iterator
  begin() {
    if (_M_cached_begin._M_has_value())
      return {this, _M_cached_begin._M_get(_M_base)};
   
    auto __it = ranges::find_if(_M_base, std::ref(*_M_pred));
    _M_cached_begin._M_set(_M_base, __it);
    return {this, std::move(__it)};
  }
};

由于第一次调用begin()时需要在filter_view内部设置缓存值,这使得begin()无法被const限定。

值得注意的是,其他具有类似时间复杂度要求的范围适配器包括drop_viewdrop_while_viewsplit_viewreverse_view和C++23的chunk_by_view

其中,drop_while_viewsplit_viewchunk_by_view从不 const-iterable,因为它们没有 const 限定的 begin(),就像 filter_view

【讨论】:

  • 这真的很有帮助,尤其是其他类似课程的列表。谢谢你。
猜你喜欢
  • 1970-01-01
  • 2020-07-06
  • 1970-01-01
  • 2022-06-09
  • 1970-01-01
  • 2020-01-11
  • 2020-08-08
  • 2016-03-15
  • 2017-03-04
相关资源
最近更新 更多