c++ - 循环边缘的 C++ 习惯用法
问题描述
问题 1:假设您有一个包含 n 个浮点数的数组,并且您想要计算一个包含三个元素的 n 个运行平均值的数组。中间部分很简单:
for (int i=0; i<n; i++)
b[i] = (a[i-1] + a[i] + a[i+1])/3.
但是您需要有单独的代码来处理案例i==0
和i==(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++ 对此没有魔锤。这是将卷积滤波器应用于信号/图像的常见问题——在内核超出图像支持的图像边界处你会做什么?
通常,您要避免两件事:
- 超出范围的数组索引(您必须避免),以及
- 特殊计算(这是不优雅的,并且由于额外的分支导致代码变慢)。
通常有以下三种方法:
- 避免边界——这是最简单的方法,通常就足够了,因为边界情况只占问题的一小部分,可以忽略不计。
- 扩展缓冲区的边界——向数组添加额外的填充列/行,以便可以在边缘使用在一般情况下使用的相同代码。当然,这引发了在填充中放置什么值的问题——这通常取决于您正在解决的问题,并在下一个方法中考虑。
- 边界处的特殊计算——这就是您在示例中所做的。当然,您如何做到这一点取决于问题,并引发与先前方法类似的问题——当我的过滤器(在您的情况下为平均过滤器)超出数组支持时,正确的做法是什么?我应该将哪些值视为数组支持之外的值?大多数图像过滤器库都提供了某种形式的外推选项——例如:
- 假设值为零或其他一些常数(定义
a[i] = 0
ifi < 0 || i >= n
), - 复制边界值(例如
a[i] = a[0]
ifi < 0
和a[i] = a[n-1]
ifi >= n
) - 包装值(定义
a[i] = a[(i + n) % n]
——在某些情况下有意义——例如,纹理过滤器) - 镜像边界((例如
a[i] = a[abs(i+1)]
ifi < 0
和a[i] = a[2n - i -1]
ifi >= n
) - 其他特殊情况(你做什么)
- 假设值为零或其他一些常数(定义
在合理的情况下,最好将特殊情况与一般情况分开(就像你做的那样),以避免不优雅和缓慢的一般情况。人们总是可以在函数或运算符中包装/隐藏特殊情况和一般情况(例如,重载 operator[]
),但这只会像任何人为的 C++ 习语一样掩盖问题。在多线程环境(例如 CUDA / SIMD)中,您可以做一些其他技巧来预加载越界值,但您仍然遇到同样的问题。
这就是为什么程序员在指代任何类型的特殊情况编程时使用短语“边缘情况”,并且通常是时间槽和恼人错误的来源。一些有效支持越界数组索引异常处理的语言(例如Ada)可以使代码更漂亮,但仍然会带来同样的痛苦。
推荐阅读
- r - 如何按ID排序,然后检测同一ID内的具体差异,然后将所有内容显示在一个数据框中?
- javascript - 赛普拉斯和设置变量
- regex - 使用转义管道分隔符解析正则表达式
- reactjs - 如何将受信任的证书添加到我的 React 项目。?
- ios - 应用内的 Touch ID/Face ID 身份验证
- python - 使用服务解决验证码时遇到问题
- bash - 在 WSL 中,为什么 "[]$ cd /mnt" 有效,但 "[mnt]$ cd /c" 给出错误?
- android - AndroidViewModel的Android getViewModelStoreOwner,没有零参数构造函数错误
- postgresql - Postgres Debezium 不发布记录的先前状态
- java - Mockito 在运行测试时找不到定义的类