【问题标题】:Range for loop with multiple containers具有多个容器的循环范围
【发布时间】:2016-11-24 10:02:02
【问题描述】:

假设我有 2 个(或更多)要同时迭代的容器 - 例如,计算两个向量的点积:

std::vector<double> vector1;
std::vector<double> vector2;    // identical size to vector1

在两个(或所有)容器上同时指定 range-for 循环的首选 C++11 方法是什么?它是否涉及在 range-for 循环中选择一个容器/迭代器来编写速记(即for ( auto i : c )),而所有其他容器/迭代器都必须长期处理?是否有任何理由将来无法扩展语法以支持两个/所有容器的简写,如下所示......这看起来真的很可读:

double dotProduct( 0.0 );
for ( auto const & value1 : vector1, auto const & value2 : vector2 )  // illegal!
{
    dotProduct += value1*value2;
}

【问题讨论】:

  • 从表面上看,是的......但是那里的任何答案(a)可读和(b)一般和(c)基于标准c ++?不是我看到的。从 C 的早期开始,可以写for ( i=0, j=0, k=0; i &lt; N; i++, j++, k++ )(现在仍然可以写),非常简洁。为什么逗号运算符没有扩展为允许在 for 循环内声明多个变量 - 允许 1,但为什么只有 1?
  • 看看miterator。使用范围会变得更容易,这不会在 C++20 之前出现。
  • 由于通过提出这个问题学到了很多,但未能回答其背后的核心问题,并且在 cmets 中没有足够的空间来表达这个问题,我提出了一个新问题here

标签: c++ c++11


【解决方案1】:

在其他(通常是函数式)语言中,这是通过使用名为 zip 的函数来完成的。例如,Python 有一个内置的 zip,它遍历其参数并返回一个元组:

for i in zip( [1,2,3], (1,2,3), { 0:0, 1:1, 2:2 } ): 
    l,t,d = i 
    print("list item: %d, tuple item %d, dict item %d" % (l,t,d) )      

您可以使用 C++ 中的范围库来获得该功能,例如Boost.RangeEric Niebler's rangev3。不幸的是,范围没有在 C++17 标准中投票,但我永远不会在没有范围库的情况下开始一个项目。在 Boost.Range 中,该函数被称为 combine:

#include <boost/range/combine.hpp>
#include <boost/tuple/tuple.hpp>
#include <iostream>
#include <vector>
#include <list>

int main(int, const char*[])
{
    using namespace boost;

    std::vector<int> const v{0,1,2,3,4};
    std::list<char> const  l{'a', 'b', 'c', 'd', 'e'};

    for(auto const& i: combine(v, l))
    {
        int ti;
        char tc;
        boost::tie(ti,tc) = i;
        std::cout << '(' << ti << ',' << tc << ')' << '\n';
    }

    return 0;
}

使用 C++17,您可以将 std::tie 替换为结构化绑定,并使用 std::tie 删除那种不寻常的“初始化”。

  for(auto const& [ti,tc] : boost::combine(v, l)) {
     std::cout << '(' << ti << ',' << tv << ')' << '\n';
  }

虽然我很遗憾 C++17 中没有包含范围,但我认为结构化绑定是一个巨大的进步,并将严重改变代码的编写方式。在标准中包含范围将使它们更受欢迎,并将它们从许多人反对的第三方库提升为 C++ 程序员应该知道的标准特性。

【讨论】:

  • 一个完全有效的答案......但有点令人失望。不应该是std::tie(ti,tc)=i吗?我觉得不满意的不是你的错 - 这是 C++ 语法的错,它严重转向(a)使用(b)技巧和(c)单个迭代器组合事物......而不是(更简单)多个同步迭代器.鉴于我对我的问题的评论中显示的原始 C 语法,我很惊讶 C++ 在支持for (int i=0,j=0;...) 时不支持for (int i=0,int j=0;...)。我很想知道是否有令人信服的理由不这样做。
  • @omatai 为什么令人失望?即使在 C++98 代码中,您也可以使用 boost::combine。关于“技巧”——查看 Python 版本——还有组合事物的“技巧”,甚至 Haskell 也有 zip 的“技巧”。而且我认为这不是缺点——为什么要用可以在库中轻松实现的东西污染语言?
  • 可读性,混乱。如果我在五年内遇到我建议的(非法)代码或此代码,我会立即知道我的建议发生了什么,并且需要应用更多的智慧和信念来消化它(完全有效)版本(不冒犯@Jens)。所以我失望的不是这个(完全有效的)答案,而是 C++ 语法未能支持我认为更简单、更干净的东西。
  • 您可能会更新答案以指出刚刚采用的 C++17 结构化绑定会将其从 for(auto const&amp; i : boost::combine(v, l)) { int ti; char tc; std::tie(ti,tc) = i; use(ti,tc); } 简化为 for(auto const&amp; [ti,tc] : boost::combine(v, l)) { use(ti,tc); }。我没有仔细研究过boost::combine,但如果它与std::tie 一起使用,它应该可以与结构化绑定一起使用。
  • @omatai 我认为 for-each 循环 for(auto const&amp; i: combine(v,l)) 比使用多个索引的循环更具表现力。我可以将 for-each 直接翻译成它的语义,但是对于 C 风格的方法,我必须扫描和解析主体以找出它只是迭代了多个集合。我认为大多数 C 风格的循环如果不是 for-each 循环,应该用(STL 风格的)算法替换。考虑更复杂的示例,例如 for(auto const&amp; i: v | filtered(pred) | transformed(f)),它非常具有表现力,并将其与 C 风格的循环进行比较。
【解决方案2】:

我知道这个问题很老了,但它仍然是谷歌的第一个结果。而且由于接受答案中的第二个解决方案不像 cmets 中提到的那样工作,这里是 C++17 的一个很好的解决方案,包括 main 中的一个示例:

#include <tuple>
#include <type_traits>

//#define ALT2

#ifndef ALT2
template<typename T, std::size_t i = 0, std::size_t j = std::tuple_size<T>::value>
struct tuple_compare {
    static bool
    one_equal(T const& lhs, T const& rhs) {
        if constexpr(i == j) return false;
        else {
            return (std::get<i>(lhs) == std::get<i>(rhs) ||
            tuple_compare<T, i + 1, j>::one_equal(lhs, rhs));
        }
    }
};
#endif

template<typename... Conts>
struct container_ref_tuple {
    static auto constexpr get_begin{[](auto&&... args){return std::make_tuple(begin(args)...);}};

    typename std::invoke_result<decltype(&std::forward_as_tuple<Conts...>), Conts&&...>::type m_refs;

    struct iterator {
        typename std::invoke_result<decltype(get_begin), Conts&&...>::type m_iterators;

        decltype(auto)
        operator++() {
            apply([](auto&... args) {((++args), ...);}, m_iterators);
            return (*this);
        }

        #ifndef ALT2
        //Alternative 1(safe)
        //will stop when it reaches the end of the shortest container
        auto
        operator!=(iterator const& rhs) const {
            return !tuple_compare<decltype(m_iterators)>::one_equal(m_iterators, rhs.m_iterators);
        }
        #else
        //Alternative 2 (probably faster, but unsafe):
        //use only, if first container is shortest
        auto
        operator!=(iterator const& rhs) const {
            return std::get<0>(m_iterators) != std::get<0>(rhs.m_iterators);
        }
        #endif

        auto
        operator*() const {
            return apply([](auto&... args){return std::forward_as_tuple(*args...);}, m_iterators);
        }
    };

    auto
    begin() const {
        return iterator{apply(get_begin, m_refs)};
    }

    #ifndef ALT2
    //Alternative 1(safe)
    //will stop when it reaches the end of the shortest container
    static auto constexpr get_end{[](auto&&... args){return std::make_tuple(end(args)...);}};
    auto
    end() const {
        return iterator{apply(get_end, m_refs)};
    }
    #else
    //Alternative 2 (probably faster, but unsafe):
    //use only, if first container is shortest
    auto
    end() const {
        iterator ret;
        std::get<0>(ret.m_iterators) = std::end(std::get<0>(m_refs));
        return ret;
    }
    #endif
};

template<typename... Conts>
auto
make_container_ref_tuple(Conts&&... conts) {
    return container_ref_tuple<Conts...>{std::forward_as_tuple(conts...)};
}

#include <array>
#include <iostream>
#include <list>
#include <vector>

int
main(int argc, char** argv) {
    std::array integers{1, 2, 3, 4, 5, 6, 7, 8};
    std::list prime{2, 3, 5, 7, 11, 13, 17, 19, 23};
    std::vector chars{'a', 'b', 'c'};

    for(auto&& [i, p, c] : make_container_ref_tuple(integers, prime, chars)) {
        std::cout << i << ' ' << p << ' ' << c << '\n';
        std::swap(i, p);
        ++c;
    }

    std::cout << "New: \n";

    for(auto&& [i, p, c] : make_container_ref_tuple(integers, prime, chars)) {
        std::cout << i << ' ' << p << ' ' << c << '\n';
    }

    return 0;
}

【讨论】:

    【解决方案3】:

    一个干净的替代方法是使用带有仅转发索引的函数的 lambda(这避免了 begin/ends 等):

    template <typename Container, typename Functor>
    void ForIndices (const Container & c, Functor && f)
    {
        for (size_t i = 0; i < c.size(); ++i)
            f(i);
    }
    

    你可以像这样在你的例子中使用它:

    double dotProduct = 0;
    ForIndices(vector1, [&](int i){dotProduct += vector1[i]*vector2[i];});
    

    Demo (C++11)

    【讨论】:

      猜你喜欢
      • 2020-02-23
      • 1970-01-01
      • 2019-01-05
      • 1970-01-01
      • 1970-01-01
      • 2017-02-10
      • 1970-01-01
      • 1970-01-01
      • 2019-07-18
      相关资源
      最近更新 更多