首页 > 解决方案 > 循环边缘的 C++ 习惯用法

问题描述

问题 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<3的情况需要单独处理。

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

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

标签: c++loopsfor-loopdesign-patterns

解决方案


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

通常,您要避免两件事:

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

通常有以下三种方法:

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

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

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


推荐阅读