【问题标题】:How to avoid code duplication when iterating over either a vector<int> or numeric range?迭代向量<int> 或数值范围时如何避免代码重复?
【发布时间】:2020-05-18 09:19:05
【问题描述】:

我需要在循环中执行一段相当复杂的代码,但循环不是在向量上,就是在整数的数值范围上。关于循环类型的决定是在运行时做出的:

if(!int_vector_provided){
   for(int i=0;i<N;++i){ // iterate over a numeric range
      // complex code depending on i
   }
} else {
   for(int i: int_vector){ // iterate over a vector
      // the same complex code
   }
}

问题在于“复杂代码”很难重构为函数,因为它依赖于许多局部和全局变量。将此代码设置为捕获 lambda 也是不可取的,因为这部分对性能至关重要。 如果使用数字范围,它通常非常大(数百万),因此创建此大小的连续数字的向量非常低效。

实际上我需要的是一对迭代器,它们可以分配给向量的开始/结束或数值范围的开始/结束。比如:

SomeCleverIterator b,e;

if(int_vector_provided){
   b = int_vector.begin();
   e = int_vector.end();  
} else {
   // Iterators to numeric range
   // may be boost::counting_range(0,N) ???
   // but how to make boost::range iterators and 
   // to vector<int>::iterator convertible to the same type??
   b = ???;
   e = ???;
}

for(SomeCleverIterator it=b;it!=e;it++){
      // complex code
}

我尝试使用boost::counting_range,但它的迭代器不能转换为vector&lt;int&gt;::iterator,所以它没有帮助。

我认为这是创建带有迭代器的自定义类模板并为向量和整数对显式实例化它的唯一方法,但这对于这种“微不足道”的问题来说似乎有点过头了。

有没有更好的办法?

【问题讨论】:

  • 为什么不将向量迭代为数字范围 [0, size())?如果您像这样更改性能关键代码,也要小心。将 if/else 移入循环可能会与分支预测交互,并可能对性能产生负面影响。
  • @midor 因为向量可能包含任何非连续整数,例如 {1,100,150,1000}
  • XY 问题。而不是int_vector_provided,应该有2个单独的函数,“复​​杂代码”部分应该被提取到一个辅助内联函数中。或提供minimal reproducible example 详细说明您的情况。
  • 这看起来像是一个经典的 XY 问题。如果您在同一个函数中有两个循环,它们具有相同的主体,那么重构以将“复杂代码”放在一个函数中是微不足道的。如果“复杂代码”取决于调用者中的变量,请将它们作为参数传递(或者,最坏的情况,创建一个包含一组对它们的引用的结构,并将其作为参数传递)。如果“复杂代码”依赖于在调用者之前声明的静态,那么将一个新函数直接放在上面将具有相同静态的可见性。
  • 您已经意识到真正的问题是什么。从长远来看,全局变量总是会咬你。最好不要再拖延它并修复它。

标签: c++ iterator range code-duplication


【解决方案1】:

由于您拥有不同类型的相同代码,因此需要模板。

而且由于它只会在那个函数中使用,所以通用 lambda 是最方便的:

auto f = [&](auto&& range) {
    for (int i : range) {
        // complex code depending on i
    }
};
if (int_vector_provided)
    f(int_vector);
else
    f(std::ranges::iota_view(0, N));

我也用过 C++20 std::ranges::iota_view

当然,这也可能是 flag-envy 的情况,在这种情况下,您应该将接口拆分为两个不同的函数,可能由一个通用实现支持。

此外,应该最小化全局状态,尤其是可变状态。

【讨论】:

    【解决方案2】:

    问题在于“复杂代码”很难重构为函数,因为它依赖于许多局部和全局变量。

    这绝对是您应该重构此代码的原因。

    将这段代码设为捕获 lambda 也是不可取的,因为这部分对性能至关重要。

    您是否测量过捕获 lambda 的性能较差?我认为它应该和直接写代码一样快,这绝对是最简单的解决方案。

    话虽如此,我认为您的问题的关键是多态性。由于您只需要处理两种不同的类型,我建议您查看std::variant。您将使用std:: variant&lt;typename std::vector&lt;int&gt;::iterator, int&gt; 并由访问者执行所有操作。

    编辑: 技术含量低的解决方案:

    int index = 0;
    const int end = int_vector_provided ? int_vector.size() : N;
    for(; index < end; ++index) {
        int to_use = int_vector_provided ? vector[index] : index;
        // Do calculations with to_use
    }
    

    这会在 int_vector_provided 上添加无关的 bool 检查,但我认为优化器在 const 上会很好地工作(我认为 const 在性能方面会产生显着差异)。

    【讨论】:

    • 在这种情况下捕获 lambda 会降低性能。不是很关键,但可以衡量。我想避免它。反对使用变体和访问者的另一点是代码本身很复杂(这是一个非常重要的数字代码),因此我想尽可能减少“样板”。
    • 带有variant 的样板文件不会很大。您有 3 个访问者,一个用于比较,一个用于增量(它们都是微不足道的),一个用于取消引用迭代器。之后你在这两种情况下都有一个 int 并且之后的代码完全相同。
    • @yesint 我添加了一个技术含量非常低的解决方案。
    【解决方案3】:

    让我想起了一个更笼统的question 我曾经问过迭代/生成器。直到 C++20 才真正有任何令人满意的普遍令人满意的解决方案。现在,您可能可以使用产生下一个元素并擦除其类型的协程来做到这一点,但我还没有编写代码。

    无论如何,在您的情况下,代码中的问题归结为两个核心点:

    1. 你有太多的上下文,你无法分解出一个函数
    2. 你不能有一个可以同时保存两个迭代器的变量,因为形成向量的迭代器是一个迭代器

    您可以使用基于索引的迭代对两者进行迭代,但您必须在每次迭代中做出决定,是使用 i 还是 v[i]。解除条件可能会更快。

    对此还有一个复杂的解决方案,即实现一个迭代器包装器,它会擦除​​迭代器的类型,但除非你已经有类似的东西可用,否则我建议不要使用那个解决方案,因为它会很复杂,很难做到正确,而且可能不成比例。

    您尚未发布上下文,因此我无法确定这在多大程度上是可能的,但我觉得我能给您的最佳建议是在您触及此问题之前重构其余代码。上下文的复杂性很可能使这种重复变得如此痛苦,而不是重复本身。提出这样的检查是一种常见的优化,并不总是对可维护性/可读性有害。

    【讨论】:

    • 是的,“生成器”,就像在 Python 中一样,可以一劳永逸地解决这些问题。但是,就我而言,务实的​​解决方案可能是重构循环中的混乱并使其成为函数或可调用类。
    猜你喜欢
    • 2011-11-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-29
    • 1970-01-01
    • 1970-01-01
    • 2014-03-07
    相关资源
    最近更新 更多