【问题标题】:Insert into vector with conditional iterator使用条件迭代器插入向量
【发布时间】:2019-01-06 18:37:42
【问题描述】:

假设我有一个带有各种条目的vector,我想将它们插入另一个向量,同时忽略满足条件的条目。

例如,我想插入一个向量,同时省略所有三个。

{1, 3, 2, 3, 4, 5, 3} -> { /* previous content, */ 1, 2, 4, 5}

到目前为止我想出的使用std::partition,它不保留相对顺序并重新排列源向量。

std::vector<int> source({1, 3, 2, 3, 4, 5, 3});
std::vector<int> target;

auto partition = std::partition(std::begin(source),
                   std::end(source), [](const auto& a) { return a == 3; });
target.insert(std::begin(target), partition, std::end(source));

我正在寻找的更像是一个迭代器,它检查条件并在条件不满足时继续前进。像这样的:

target.insert(std::begin(target),
              conditional_begin(source, [](const auto& a) { return a != 3; }),
              conditional_end(source));

我想conditional_end 函数是必要的,因为std::end 将返回与conditional_begin 不同的迭代器类型。

也许我忽略了一些东西,所以我的问题是:

  • 标准库是否提供类似的功能?
  • 是否有其他简单的方法可以实现我的目标?
  • 有没有一种简单的方法来实现条件迭代器功能?

【问题讨论】:

  • 你看过std::copy_if吗?
  • 不,没有考虑过。谢谢!

标签: c++ iterator


【解决方案1】:

有没有其他简单的方法可以实现我的目标?

是的,标准已经内置了此功能。您要查找的功能是std::copy_if

std::vector<int> source({1, 3, 2, 3, 4, 5, 3});
std::vector<int> target;

std::copy_if(source.begin(), 
             source.end(), 
             std::back_inserter(target), [](auto val){ return val != 3; });

这里,std::back_inserter(target) 将为谓词返回 true 的每个元素在 target 上调用 push_back

【讨论】:

  • 这里的back_inserter 可能会导致target 上的多次分配,这对于性能关键代码来说可能不是最佳的。
  • @ChrisDrew 没错,但如果你不知道要复制多少元素,你要么必须猜测,要么让它几何增长。
  • 或者您可以计算出首先要复制多少个元素,方法是计算它们,或者在插入它们之前过滤它们。您必须测量哪个可能(或可能不会)更快。我只是认为值得关注。
  • 要保留多少,到底要保留多少,很大程度上取决于调用代码的上下文。
  • @eucristian 当然可以。如果类型很大,请通过const auto&amp;。我在这里只使用了auto,因为我们正在处理int,创建一个引用可能比制作一个副本需要更多的工作。
【解决方案2】:

是的,您可以创建一个自定义迭代器来满足您的需求,但目前使用标准 C++ 创建自定义迭代器有点乏味。它看起来像这样:

template <typename Itr, typename F> 
struct ConditionalIterator {
  Itr itr;
  Itr end;
  F condition;

  using value_type = typename Itr::value_type;
  using difference_type = typename Itr::difference_type;
  using pointer = typename Itr::pointer;
  using reference = typename Itr::reference;
  using iterator_category = std::forward_iterator_tag;

  ConditionalIterator() = default;
  ConditionalIterator(Itr itr, Itr end, F condition): itr(itr), end(end), condition(condition) {}

  bool      operator!=(const ConditionalIterator &other) const { return other.itr != itr; }
  reference operator*() const { return *itr; }
  pointer   operator->() const { return &(*itr); }

  ConditionalIterator& operator++() {
    for (; ++itr != end;) {
      if (condition(*itr))
        break;
    }
    return *this;
  }
  ConditionalIterator operator++(int) {
    ConditionalIterator ret(*this);
    operator++();
    return ret;
  }
};

然后您可以创建类似于您要求的conditional_beginconditional_end 辅助函数。唯一的问题是std::vector::insert 期望两个迭代器具有相同的类型。如果我们使用 lambda 作为我们的条件,那么这将是我们的条件迭代器类型的一部分。所以我们需要将 lambda 传递给两个辅助函数,以便它们返回具有匹配类型的迭代器:

template <typename C, typename F> 
auto conditional_begin(const C &source, F f) {
  return ConditionalIterator<typename C::const_iterator, F>(source.begin(),
                                                            source.end(), f);
}

template <typename C, typename F> 
auto conditional_end(const C &source, F f) {
  return ConditionalIterator<typename C::const_iterator, F>(source.end(),
                                                            source.end(), f);
}

你可以用这样的 lambda 调用:

auto condition = [](const auto &a) { return a != 3; };
target.insert(std::begin(target),
              conditional_begin(source, std::ref(condition)),
              conditional_end(source, std::ref(condition)));

Live demo.

我的crude tests 显示,在这种情况下,这最终比简单地使用copy_ifback_inserter 快得多,因为std::vector::insert 在插入之前首先计算出要分配多少内存。仅使用back_inserter 将导致多次内存分配。性能差异将取决于评估条件的成本。您可以通过在使用copy_if 之前使用count_if 保留足够的空间来获得相同的加速:

auto count = static_cast<size_t>(std::count_if(source.begin(),
                                 source.end(), condition));
target.reserve(target.size() + count);

std::copy_if(source.begin(),
             source.end(),
             std::back_inserter(target), condition);

Live demo.

【讨论】:

  • 不错的答案,+1。我不得不想知道矢量如何摆脱这一点。输入迭代器通常只能迭代一次,因此它们必须为其他类型的迭代器重载。
  • 我喜欢你的回答。我记得不久前玩过自定义迭代器,这很痛苦。你能解释一下operator++(int)的目的是什么吗?
  • @KorbenDose 那是post-increment operator。如果您执行customItr++,这就是所谓的。如果您执行++customItr,则调用预增量运算符。我在那里添加它是因为我认为它应该为forward iterators 实现。但不确定在实践中是否需要。
  • @NathanOliver 是的,我认为vector::insert 被证明对前向迭代器具有更好的复杂性。我假设它使用std::iterator_traits 来判断它是什么类型的迭代器。
【解决方案3】:

由于范围即将标准化,这是使用 range-v3 的替代方案,该参考库是提案的参考库:

#include <range/v3/view/concat.hpp>
#include <range/v3/view/filter.hpp>

using namespace ranges;

const std::vector<int> source{1, 3, 2, 3, 4, 5, 3};
const std::vector<int> target = view::concat(source,
    source | view::filter([](auto i){ return i != 3; }));

【讨论】:

猜你喜欢
  • 2012-01-05
  • 2015-12-22
  • 2020-09-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-29
  • 2012-03-19
  • 1970-01-01
相关资源
最近更新 更多