【问题标题】:How can I create iterators of a filtered vector?如何创建过滤向量的迭代器?
【发布时间】:2021-03-16 22:27:43
【问题描述】:

假设我有一个名为 spot_deals 的向量 SpotDeal 是一个类:

class SpotDeal
{
public:
    int deal_id_; // primary key, and vector is sorted by id
    string ccy_pair_; // ccy pair, e.g. GBPUSD, AUDUSD
    double amount_;
}

假设我需要将spot_deals 的两个子集传递给函数foo 进行一些计算。但是,我可以制作副本,这会耗费内存和时间。实际上foo 只需要交易的迭代器。那么我可以制作vector<SpotDeal>的2个迭代器,即it1it2并将它们传递给foo吗?

spot_deals 的两个子集可以被ccy_pair_ 过滤,例如GBPUSD 和 AUDUSD 的交易,或其他条件。所以我正在寻找一种方法来定义一个由向量​​和 lambda 函数定义的迭代器(虽然可以等效地是一个仿函数)。

有没有办法编写一个辅助函数make_filtered_iterator 以便我可以得到类似下面的东西?

auto it1 = make_filtered_iterator(spot_deals, filter_lambda1);
auto it2 = make_filtered_iterator(spot_deals, filter_lambda2);
foo(it1, it2);

【问题讨论】:

  • @wallyk ??我正在寻找一种方法来实现make_filtered_iterator 辅助功能...还没有什么可以尝试...
  • @perreal thx 指出.. 但是 boost 有点太重了,我正在尝试使用 STL

标签: c++ lambda filter iterator containers


【解决方案1】:

答案肯定是“是的”。 STL 风格的 C++ 迭代器可以用来做各种技巧。一个常见但基本的方法是为 std::map 创建一个迭代器,在取消引用时只给出键或值。

在您的特定情况下,一个简单的实现可能是这样的:

template <typename BaseIterator>
struct filtered_iterator : BaseIterator
{
    typedef std::function<bool (const value_type&)> filter_type;

    filtered_iterator() = default;
    filtered_iterator(filter_type filter, BaseIterator base, BaseIterator end = {})
        : BaseIterator(base), _end(end), _filter(filter_type) {
        while (*this != _end && !_filter(**this)) {
            ++*this;
        }
    }

    filtered_iterator& operator++() {
        do {
            BaseIterator::operator++();
        } while (*this != _end && !_filter(**this));
    }

    filtered_iterator operator++(int) {
        filtered_iterator copy = *this;
        ++*this;
        return copy;
    }

private:
    BaseIterator _end;
    filter_type _filter;
};

template <typename BaseIterator>
filtered_iterator<BaseIterator> make_filtered_iterator(
        typename filtered_iterator<BaseIterator>::filter_type filter,
        BaseIterator base, BaseIterator end = {}) {
    return {filter, base, end};
}

我为end 设置了一个默认值,因为通常您可以为此使用默认构造的迭代器。但在某些情况下,您可能只想过滤容器的一个子集,在这种情况下,指定结尾会很容易。

【讨论】:

  • 我在BaseIterator end = {} 有点迷路了。我通常理解end迭代器位于容器的最后一个真实元素之后,因此内存中的地址因不同的容器对象而异,它仅用于地址比较(即该地址中的内容无效)。这样的迭代器怎么会默认由空大括号构造?
  • @athos:取决于容器。在某些情况下 iterator() 可用作“结束”,而在其他情况下则不是。如果您不喜欢它,请删除默认值。
  • 如果您因为我指出的原因编辑了代码,您应该提及。
  • 好的。似乎我需要更多地了解迭代。谢谢!
  • @PasserBy:我想感谢纽约大学的 Mark Meretzky,因为他让我顿悟到 C++ 迭代器基本上可以做任何事情。您的注释让我注意到我的原始代码中有一个错误,但该代码无论如何都是 100% 未经测试的,所以这对演示并不重要。
【解决方案2】:

是的,可以创建一个迭代器类型。但是,我怀疑您的问题是 XY 问题的一个示例 (https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) - 您想找到一种对向量 (X) 的两个子集进行不同操作的方法,已决定解决方案必须涉及实现一个特殊用途的迭代器(Y),并询问了如何做 Y 而不是 X。我将提供一个选项来做 X 而无需做 Y。

我建议使用标准算法std::stable_partition() 将容器分成两个范围会更容易。

 auto false_partition = std::stable_partition(your_vector.begin(), your_vector.end(), your_filter);

向量的begin()end() 迭代器不会改变(即不会失效),但它们之间的元素会被重新组织成两个范围,这样your_filter 返回true 的元素在前面your_filter 为其返回 false 的元素集。 false_partition 因此同时是第一个范围的“结束”迭代器和第二个范围的开始。每个范围内元素的顺序与原始向量中的顺序相同。

这些可以如下使用

 //   a loop to operates on the elements for which your_filter returned true

 for (auto i = your_vector.begin(); i != false_partition; ++i)
 {
      // do whatever
 }

 //   a loop to operates on the elements for which your_filter returned false

 for (auto i = false_partition; i != your_vector.end(); ++i)
 {
      // do whatever
 }

在 C++11 之前,auto 关键字可以替换为适当的迭代器类型(例如 std::vector&lt;int&gt;::iteratorstd::vector&lt;int&gt;::const_iterator,具体取决于您是否希望使用迭代器更改元素)。

【讨论】:

  • 感谢回复,但我的问题是这样的:假设我有一个AakkafdBlkjfDadfkljXafd 的字符串,我想生成一个map&lt;char, string&gt; 作为{['A', "akkafd"], ['B', "lkjf"], ['D', "adfklj"], ['X', "afd"]}
  • 感谢您确认您提供了一个 XY 问题,尽管 X 与我预期的不同。你真的不需要为此创建一个特殊的迭代器。你所描述的是一个微不足道的解析练习,你想多了。基本算法:查找大写字母。从一个空字符串开始。将输入中的字符附加到字符串,直到找到另一个大写字母或输入结尾。做your_map[uppercase_letter] = copied_string。重复直到完成。您可以使用引用输入字符串元素的单个迭代器来做到这一点。
  • 如何找到“下一个”大写字符?当然这可以通过循环来完成,我正在尝试出一个 STL 风格的解决方案
  • “STL 风格的解决方案”(无论您认为是什么意思)并不一定意味着创建时髦的迭代器。查看各种标准算法的规范。您很可能能够使用标准算法的组合来做您想做的事——这将被称为“STL 风格的解决方案”。诀窍是以尽可能简单明了的方式做你想做的事。
【解决方案3】:

我可能会建议这个问题的观众参加 Eric Nibbler 的 rangev3 速成课程,因为这是 C++20 标准库采用的范例。

https://github.com/ericniebler/range-v3

如何进行过滤器迭代:

for (auto element : spot_deals | views::filter([](auto i) { return condition(i); }))
{ //....
}

【讨论】:

    【解决方案4】:

    我不会使用指向原始向量的迭代器,因为它们无法传达子集的大小。 (基本上,每个子集还需要一个迭代器来表示子集的结尾。)截至C++20,我将使用Ranges library 中的ranges,如v.oddou's 中所述回答。更具体地说,对于您的用例,我将使用范围适配器 std::views::filter,如下所示:

    auto gbpusd = [](const auto& sd) { return sd.ccy_pair_ == "GBPUSD"; };
    auto audusd = [](const auto& sd) { return sd.ccy_pair_ == "AUDUSD"; };
    
    auto range1 = spot_deals | std::views::filter(gbpusd);
    auto range2 = spot_deals | std::views::filter(audusd);
    
    foo(range1, range2);
    

    此解决方案不会为过滤的现货交易创建临时向量,因为视图适配器会创建不包含元素的范围。结果范围 range1range2 只是向量 spot_deals 上的视图,但具有自定义的迭代行为。

    foo() 的声明有点棘手,因为范围的数据类型相当复杂。因此,我将使用占位符类型auto 作为函数参数,从而使foo() 成为function template

    void foo(auto& r1, auto& r2) {
        for (auto const& sd : r1)
            std::cout << sd.deal_id_ << std::endl;
        for (auto const& sd : r2)
            std::cout << sd.amount_ << std::endl;
    }
    

    (或者,您可以通过引用foo() 来传递spot_deals,并在foo() 中声明过滤范围。)

    Code on Wandbox

    【讨论】:

      【解决方案5】:

      碰巧,我最近正在研究这个确切的问题。事实证明,过滤是容器上相当多的操作中最复杂的,也包含最多的陷阱。

      template<typename Range, typename Pred>
      class filter
      {
      public:
          friend class const_iterator;
          class const_iterator : public std::iterator_traits<typename Range::const_iterator>
          {
              using underlying = typename Range::const_iterator;
          public:
              auto operator*() {return *u;}
              const_iterator& operator++()
              {
                  ++u;
                  normalize();
                  return *this;
              }
              const_iterator operator++(int)
              {
                  auto t = *this;
                  u++;
                  normalize();
                  return t;
              }
              bool operator==(const const_iterator& rhs) const {return u == rhs.u;}
              bool operator!=(const const_iterator& rhs) const {return !(*this == rhs);}
          private:
              friend filter;
              const_iterator(underlying u, const filter& f) : u{std::move(u)}, f{f} {normalize();}
              void normalize()
              {
                  for(; u != f.r.end() && !f.p(*u); u++);
              }
      
              underlying u;
              const filter& f;
          };
      
          filter(const Range& r, const Pred& p) : r{r}, p{p} {}
      
          auto begin() const {return const_iterator{r.begin(), *this};}
          auto end() const {return const_iterator{r.end(), *this};}
      
      private:
          const Range& r;
          Pred p;
      };
      

      我们将其用作(使用 c++17 指南)

      vector<int> v{1, 2, 3, 4, 5};
      auto f = filter(v, [](int x){return x & 1;});
      for(auto i : f)
          // all i in v that is odd
      

      让我解释一下陷阱:

      • 第一个元素可能被过滤掉,*r.begin() 可能不是过滤范围内的元素。这意味着必须在构造时检查迭代器。
      • r.end() 可能会在不使其他迭代器失效的情况下失效,也就是说,在任何迭代器中保存 r.end() 的副本以进行比较是一个逻辑错误。
      • operator== 不是很简单,引用原始范围内不同元素的两个底层迭代器可能引用过滤视图中的相同元素,因为过滤掉的元素不算作一个元素。

      【讨论】:

      • auto operator*() { return *u; } 出现编译错误,提示“错误 C3551:预期尾随返回类型”,是否需要以某种方式为 VS2013 修改代码?
      • @athos 你需要c++14或以上
      • 我在使用此代码并将带有捕获的 lambda 作为第二个参数传递给 filter() 时遇到了段错误。这可以通过将 filter::p 的类型从 const Pred&amp; 更改为 Pred(非参考)来解决。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-11-06
      • 1970-01-01
      • 1970-01-01
      • 2017-08-29
      • 2018-05-20
      相关资源
      最近更新 更多