【问题标题】:Iterating over odd (even) elements only in a range-based loop仅在基于范围的循环中迭代奇数(偶数)元素
【发布时间】:2019-07-17 22:21:30
【问题描述】:

假设我们有一个普通数组(或其他支持基于范围的循环的容器):

const int N = 8;
int arr[N] = {0, 1, 2, 3, 4, 5, 6, 7};

使用索引或迭代器,我们可以遍历奇数元素并将索引增加 2:

for (int i = 0; i < N; i+=2)
{
   std::cout << arr[i] << std::endl;
}

如何通过使用基于范围的循环并避免显式迭代器/索引和迭代跳过来获得类似的结果?像这样的:

for (const auto& v: odd_only(arr))
{
   std::cout << v << std::endl;
}

简单而优雅的解决方案是什么样的?标准库是否包含这样的内容?

【问题讨论】:

  • 我认为我们目前在 STL 中没有类似的东西,但 range-v3view::stride 可能是您正在寻找的东西(尽管我不确定它是如何工作的使用普通数组 - std::array 应该没问题)。
  • 更新:绝对适用于普通数组 (example)。
  • 如前所述,没有直接支持。如果您不想在 3rd-party 库上进行中继,您可以做的只是类似于bool isEven = false /* isOdd = true */; for(...) { if((isEven = !isEven)) { ... }; }。不过,我个人宁愿只保留原始循环...
  • 请注意,您的基于索引的示例使用&lt;。迭代器并不总是有&lt;,而且更成问题的是,在末尾创建迭代器通常是未定义的行为,因此唯一的选择是单步执行并将每个迭代器与末尾进行比较(但仅在每隔一次迭代)。因此进行迭代跳过,即使它对您隐藏。如果您不喜欢迭代跳过,则不能使用迭代器。
  • @Aconcagua:正确,distance(it, e) &lt; n(或e - it &lt; n)可以在没有未定义行为的情况下完成,而不是直接转换索引 for 循环。可能应该有一些advance_not_past(it, n, e) 函数对随机访问迭代器很有效,对其他迭代器仍然是最佳的(单次通过)。

标签: c++ c++11 range containers range-based-loop


【解决方案1】:

这并不是问题的真正答案,但是——就其价值而言——每当我遇到 ranged-for 的限制时,我都会寻找一个标准的算法解决方案。喜欢...

#include <algorithm>
#include <iostream>
#include <iterator>
#include <utility>

int main()
{
    int arr[] {0, 1, 2, 3, 4, 5, 6, 7};
    std::copy_if(
        std::begin(arr), std::end(arr),
        std::ostream_iterator<int>(std::cout, "\n"),
        [is_odd_element = true](int n) mutable {
            return std::exchange(is_odd_element, not is_odd_element);
        });
}

【讨论】:

  • 嗯...我认为从技术上讲我不应该将该 lambda 包装在 std::ref 调用中。
  • 哇!这当然太麻烦了,但还是可以的!
  • 可能这个问题太麻烦了,虽然我不知道我是否同意。但我认为这种方法可能更适合类似但更复杂的问题,因此学习思考这种解决方案很有用。更重要的是,我认为以这种方式思考解决方案是朝着我们需要思考以充分利用范围的方式迈出的一步。 (我怀疑存在不使用显式循环的基于范围的解决方案。)
【解决方案2】:

Range-v3 中有针对此问题的现成解决方案。如果您不想编写自己的实现或需要更大的灵活性(例如任意步幅),我认为这会很有用

#include <range/v3/all.hpp>

void example()
{
    int data[8] = {0, 1, 2, 3, 4, 5, 6, 7};
    for (auto i : ranges::view::stride(data, 2))
    {
        std::cout << i << std::endl;
    }
}

(复制自@hlt 评论)

【讨论】:

    【解决方案3】:

    至于你目前在问什么;我不相信任何东西还存在。现在,对于通过某个整数 N 迭代容器,我们可以执行以下操作;我们可以编写自己的for_each 类型的函数。我在下面写了一个,它就像一颗宝石!您可能还想查看std::advance 函数,因为它可能是另一种可能的实现。在编写此函数时,我正在检查自己。然而;至于 c 数组,我不确定如果没有一堆额外的代码(例如类模板、包装器等)可以做很多事情。这是我的函数。

    #include <array>
    #include <vector>
    #include <iterator>
    
    template<typename Container, typename Function>
    void for_each_by_n( Container&& cont, Function f, unsigned increment_by = 1) {
        if ( increment_by == 0 ) return; // must check this for no op
    
        using std::begin;
        auto it = begin(cont);
    
        using std::end;
        auto end_it = end(cont);
    
        while( it != end_it ) {
            f(*it);
            for ( unsigned n = 0; n < increment_by; ++n ) {
                if ( it == end_it ) return;
                ++it;
            }
        }
    }
    
    int main() {
        std::array<int,8> arr{ 0,1,2,3,4,5,6,7 };
        std::vector<double> vec{ 1.2, 1.5, 1.9, 2.5, 3.3, 3.7, 4.2, 4.8 };
    
        auto l = [](auto& v) { std::cout << v << ' '; };
    
        for_each_by_n(arr, l); std::cout << '\n';
        for_each_by_n(vec, l); std::cout << '\n';
    
        for_each_by_n(arr, l, 2); std::cout << '\n';
        for_each_by_n(arr, l, 4); std::cout << '\n';
    
        for_each_by_n(vec, l, 3); std::cout << '\n';
        for_each_by_n(vec, l, 5); std::cout << '\n';
    
        for_each_by_n(arr, l, 8); std::cout << '\n';
        for_each_by_n(vec, l, 8); std::cout << '\n';
    
        // sanity check to see if it doesn't go past end.
        for_each_by_n(arr, l, 9); std::cout << '\n';
        for_each_by_n(vec, l, 9); std::cout << '\n';
    
        return 0;
    }
    

    -输出-

     0 1 2 3 4 5 6 7
     1.2 1.5 1.9 2.5 3.3 3.7 4.2 4.8
     0 2 4 6 
     0 4
     1.2 2.5 4.2
     1.2 3.7
     0
     1.2
     0
     1.2
    

    我喜欢上面这个例子的地方在于,你不仅可以在循环中增加一些整数N;上面的函数还接受function pointerfunction objectfunctorlambda,它将执行所需的操作。

    在您的情况下,您试图通过 2 循环遍历您的容器,以获得奇数或每个偶数索引,并且在循环内您正在打印结果。在我的例子中;我正在以传递给这个函数的 lambda 的形式打印结果。

    然而,这个特定实现的唯一警告是它总是从索引 0 开始。您可以通过引入另一个 integer 参数来轻松扩展这一点,作为迭代开始位置的偏移量;但我会留给你做练习。

    目前,我们必须满足于 C++11 到 C++17 所提供的功能。在不久的将来,随着 C++20 的发布,我们应该会拥有许多新的强大功能。

    【讨论】:

    • 代码看起来很干净,可读性很强,它做的就是它所说的,它很容易维护,并且很容易扩展。对于任何具有开始和结束迭代器的容器,它在本质上仍然是通用的,其中大多数情况下几乎都是std::container 的......它不依赖于大小,它不依赖于正向、反向或两个方式迭代器,只是普通的基本迭代器。它也是通用的,因为您可以将任何功能传递给它来完成您想要的工作,就像我在打印中展示的那样。它可以是排序、搜索、写入文件等,而 lambdas 让它变得非常好。
    • 也一样。不过,随机访问迭代器有一种优化的可能性,我们可以在 constexpr 中选择 if ('if RA-tag is supported'): if(distance(it, end_it) &lt; increment_by) it += ...; /*or std::advance(...);*/ else it = end_it;
    • @Aconcagua ... 或者,更进一步并保持模块化设计:定义包含 RA 优化的算法 limit_advance(iter, end, n)
    【解决方案4】:

    不支持您的要求 - 但您可以编写自己的 even_onlyodd_only 实现。

    基本思想是围绕相关容器的普通迭代器,并在每次外部递增一次时在内部进行双倍递增:

    template <typename C, bool IsOdd>
    class even_odd_only
    {
        C& c;
    public:
        class iterator
        {
        public:
            // all the definitions required for iterator!
            // most if not all might simply be derived from C::iterator...
    
            // copy/move constructor/assignment as needed
    
            // core of the wrapper: increment twice internally!
            // just doing += 2 is dangerous, though, we might increment beyond
            // the end iterator (undefined behaviour!)additionally, += 2 only
            // is possible for random access iterators (so we limit usability)
            void operator++() { ++b; if(b != e) ++b; }
    
            // operator* and operator-> (both return *b), post-increment
            // (defined in terms of pre-increment), etc...
            // comparison: only needs to compare b iterators!
    
        private:
            C::iterator b;
            C::iterator e; // needed for comparison to avoid incrementing beyond!
            iterator(C::iterator b, C::iterator e) : b(b), e(e) { }
        };
        // const_iterator, too; possibly make a template of above
        // and derive const and non-const iterators from?
    
        even_odd_only(C& c) : c(c) { }
    
        iterator begin()
        {
            using std::begin;
            using std::end;
            using std::empty;
            auto b = begin(c);
            // should be self-explanatory:
            // skip first element in odd variant (if there is)
            if constexpr(IsOdd) { if(!empty(c)) { ++b; } }
            return iterator(b, end(c));
        };
        iterator end()
        {
            using std::end;
            return iterator(end(c), end(c));
        }
    };
    
    template <typename T>
    using even_only = even_odd_base<T, false>;
    template <typename T>
    using odd_only = even_odd_base<T, true>;
    

    照原样,它甚至可以与非随机访问甚至非双向迭代器一起工作。但特别是对于 RA 迭代器,它的效率低于经典循环(由于 operator++ 中的中间 if)。

    定义比较迭代器:总是operator==operator!=,仅对于随机访问运算符,您还可以有operator[&lt;|&gt;|&lt;=|&gt;=](→ std::enable_if)。

    您将找到有关如何编写迭代器 here 的更多详细信息 - 但请记住,当您遇到 std::iterator 本身现在已被弃用时。

    【讨论】:

    • 谢谢,这基本上是我想看到的。在我看来,添加更多关于它如何工作的解释是有意义的。这将增加答案的价值。
    • @DmytroDadyka 嗯,认为它是不言自明的...... - 但好的,添加了几行。
    猜你喜欢
    • 1970-01-01
    • 2020-06-01
    • 1970-01-01
    • 2017-03-20
    • 1970-01-01
    • 1970-01-01
    • 2014-12-03
    • 1970-01-01
    • 2015-08-27
    相关资源
    最近更新 更多