【问题标题】:C++ idiom for loop edges循环边缘的 C++ 习惯用法
【发布时间】:2021-01-25 04:08:54
【问题描述】:

问题 1:假设您有一个包含 n 个浮点数的数组,并且您想要计算一个包含三个元素的 n 个运行平均值的数组。中间部分很简单:

for (int i=0; i<n; i++)
    b[i] = (a[i-1] + a[i] + a[i+1])/3.

但是您需要有单独的代码来处理i==0i==(n-1) 的情况。这通常通过循环前的额外代码、循环后的额外代码以及调整循环范围来完成,例如

b[0] = (a[0] + a[1])/2.
for (int i=1; i<n-1; i++)
    b[i] = (a[i-1] + a[i] + a[i+1])/3.;
b[n-1] = (a[n-1] + a[n-2])/2.

即使这样还不够,因为n

问题 2。您正在从数组中读取可变长度代码(例如实现 UTF-8 到 UTF-32 转换器)。代码读取一个字节,因此可以读取一个或多个字节以确定输出。但是,在每个这样的步骤之前,它还需要检查是否已到达输入数组的末尾,如果已到达,则可能将更多数据加载到缓冲区中,或者以错误终止。

这两个问题都是循环的情况,循环的内部可以整齐地表达,但边缘需要特殊处理。我发现这类问题最容易出错和混乱的编程。所以这是我的问题: 是否有任何 C++ 习语可以概括以干净的方式包装此类循环模式?

【问题讨论】:

  • 最简单的方法是移动(非运行)平均值数组的元素少于输入(考虑到在边缘,您无法使用相同的方法计算平均值元素的数量 - 所以不要计算它们)。如果您需要平均值数组具有相同数量的元素,则需要对被平均数据的性质做出一些假设。
  • 这是几种可能的解决方案之一(例如,您也可以通过多种方式填充数组),但这只是一个简单的示例。我正在寻找解决此类问题的通用方法。
  • @Zzyzx - 没有针对此类问题的通用方法。是的,有几种方式可以使用填充。在计算移动平均值之前,也可以使用某种形式的过滤。所有方法都涉及对输入数据(或生成它的过程)的特征做出一些假设。但是没有适用于所有情况的通用/通用方法。
  • 当然。我希望以某种方式以清晰的方式封装边缘行为,同时最大限度地减少索引计算错误的可能性,并生成高效的代码。
  • 我在想range 的一些概括,这将允许“假”迭代器超出实际现有元素。

标签: c++ loops for-loop design-patterns


【解决方案1】:

高效优雅地处理边界条件任何编程语言中都是很麻烦的——C++对此没有魔锤。这是将卷积滤波器应用于信号/图像的常见问题——在内核超出图像支持的图像边界处您会做什么?

您通常要避免两件事:

  • 数组索引越界(您必须避免),以及
  • 特殊计算(不优雅并且由于额外的分支导致代码变慢)。

通常有三种方法:

  • 避免边界 -- 这是最简单的方法,通常就足够了,因为边界情况只占问题的一小部分,可以忽略不计。
  • 扩展缓冲区的边界 -- 将额外的列/行 padding 添加到数组中,以便在一般情况下使用相同的代码用在边缘。当然,这引发了在填充中放置什么值的问题 - 这通常取决于您要解决的问题,并在下一个方法中考虑。
  • 边界处的特殊计算 -- 这就是您在示例中所做的。当然,您如何做到这一点取决于问题,并引发与以前的方法类似的问题 - 当我的过滤器(在您的情况下为平均过滤器)超出数组支持时,正确的做法是什么?我应该将哪些值视为数组支持之外的值?大多数图像过滤器库provide some form of extrapolation options——例如:
    • 假设值为零或其他一些常量(如果 i &lt; 0 || i &gt;= n 则定义 a[i] = 0),
    • 复制边界值(例如,a[i] = a[0] if i &lt; 0 and a[i] = a[n-1] if i &gt;= n
    • 包装值(定义a[i] = a[(i + n) % n]——在某些情况下有意义——例如,纹理过滤器)
    • 镜像边界((例如a[i] = a[abs(i+1)] if i &lt; 0 and a[i] = a[2n - i -1] if i &gt;= n
    • 其他特殊情况(你做了什么)

在合理的情况下,最好将特殊情况与一般情况分开(就像您所做的那样),以避免一般情况不优雅和缓慢。人们总是可以在函数或运算符中包装/隐藏特殊情况和一般情况(例如,overload operator[]),但这只会像任何人为的 C++ 习语一样掩盖问题。在多线程环境(例如 CUDA / SIMD)中,您可以执行一些其他技巧来预加载越界值,但您仍然会遇到同样的问题。

这就是为什么程序员使用“边缘情况”一词来指代任何类型的特殊情况编程,并且通常是时间的沉没和恼人的错误的来源。一些有效地支持对超出范围的数组索引进行异常处理的语言(例如 Ada)可以编写更漂亮的代码,但仍然会带来同样的痛苦。

【讨论】:

  • 谢谢,“边界”会是更好的词。在某些 HPC 应用程序(有限元素?)中,不能忽略边界(它们是区域使用彼此数据的地方),这就是事情可能变得特别混乱的地方。我想知道是否有任何元编程工具可以通用方式处理这个问题。
【解决方案2】:

很遗憾,答案是否定的。 没有任何 C++ 习语可以概括以干净的方式包装此类循环模式!

【讨论】:

    【解决方案3】:

    你可以通过做这样的事情来做到这一点,但你仍然需要调整窗口大小。

    template <typename T, int N> 
    T subscript(T (&data)[N], int index) {
        if (index < 0 || index >= N) {
            return 0;
        }
        
        return data[index];
    }
    
    for (int i = 0; i < n; ++i) {
        b[i] = (subscript(a, i - 1) + subscript(a, i) + subscript(a, i + 1)) / 3.
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-06-11
      • 2011-11-04
      • 1970-01-01
      • 1970-01-01
      • 2018-11-17
      • 2018-04-07
      • 1970-01-01
      • 2015-01-22
      相关资源
      最近更新 更多