为什么很多(旧)程序使用floor(0.5 + input)而不是round(input)?

84

我认为区别在于返回值给出了关于解决平局的输入,比如这段代码

int main()
{
    std::cout.precision(100);

    double input = std::nextafter(0.05, 0.0) / 0.1;
    double x1 = floor(0.5 + input);
    double x2 = round(input);

    std::cout << x1 << std::endl;
    std::cout << x2 << std::endl;
}

输出结果为:

1
0

但最终它们只是不同的结果,选择自己喜欢的。我看到很多“老”C/C++程序使用floor(0.5 + input)而不是round(input)
这是否有历史原因?CPU最便宜吗?

20
std::round函数将四舍五入的中间值远离0。这不是数学上的均匀操作,就像floor(f + .5)一样,其中四舍五入的中间值总是向上舍入。使用floor方法的原因是在工程领域中需要进行正确舍入。 - Michaël Roy
7
正如在C++中float的round()函数中所提到的,在C++11之前,我们没有round函数。正如我在我的答案中所指出的,编写正确的自定义round函数是一个困难的问题。 - Shafik Yaghmour
16
使用 std::round() 函数将 -0.5 和 +0.5 四舍五入后的差距为 2,而使用 floor() 函数则为 1。这种情况只会在这两个值有不同符号时发生。当尝试绘制直线或选择错误的纹理像素时会非常烦人。 - Michaël Roy
20
有些编程语言和环境(包括 .NET)使用一种欺骗性的舍入方式叫做银行家舍入,其中 x.5 会舍入到最接近的偶数。因此0.5会舍入为0,而1.5会舍入为2。你可以想象在调试时这可能会引起多大的混乱。我认为解决这个恶意“特性”的方法是根本不要有 .Round() 函数,而是改为 .RoundBankers()、.RoundHalfUp()、.RoundHalfDown() 等(或者 .BankersRound() 等,但智能感知会用 .RoundBankers() 更好)。至少这样你将被迫知道该期望什么。 - user3685427
9
在金融和统计应用中,需要消除“远离零”舍入引入的微妙和系统性向上偏差,因此需要使用“银行家舍入”。几乎不可能在没有实际了解硬件浮点实现的情况下实施,因此它被选择作为C#的默认设置。 - Pieter Geerkens
显示剩余11条评论
5个回答

118

std::round 是 C++11 中引入的。在此之前,只有 std::floor 可用,因此程序员使用它。


仅翻译文本内容:没有。但它比C++11更旧。我认为C++在11之前就已经具备了它。就是这样;) - markzzz
12
C++标准库并不会自动继承C语言的标准库。需要精心挑选和选择。它花了12年时间与C99同步。 - StoryTeller - Unslander Monica
2
@haccks:确实。在我看来,直到C++11,C在数学函数方面领先于C++。 - Bathsheba
3
需要注意的是,所使用的方法对于某些输入会出错,同时C++11依赖于C99,而C++03依赖于C90,这也支持了@markzzz的观点。 - Shafik Yaghmour
1
@markzzz:你的评论很中肯(尽管有批评)。问题在于,C++标准长时间没有更新,第一个C++标准是C++98,第一个重大修订是C++11。虽然有一个小的更新版本C++03,但正如Wikipedia页面所述,它主要是一个“错误修复”版本。因此,在经过13年的间歇期后,C++11成为了追赶C标准库的第一个机会。 - Matthieu M.
std::round是银行家舍入法。这与std::floor无关。而且,std::round不适用于金融领域以外的任何其他领域...由于任何严肃的货币计数应用程序都使用某种整数表示货币价值,因此我会说std::round在现实生活中相当无用。 - Michaël Roy

22

这完全没有历史原因。这种偏差一直存在。这是对浮点运算的滥用,许多有经验的专业程序员也会犯这个错误。甚至Java团队在1.7版本之前也犯过这个错误。这些搞笑的家伙。

我的猜测是,在C ++ 11之前,一个像样的开箱即用的舍入函数并不存在(尽管C99已经拥有了他们自己的函数),但这真的不是采用所谓替代方案的借口。

问题在于:floor(0.5 + input)并不总是能恢复与相应的std::round调用相同的结果!

原因非常微妙:四舍五入的截断点,对于整数a来说,是一个二进制理性数。由于这可以精确地表示为IEEE754浮点数上的52次幂,而且此后舍入操作无论如何都是无意义的,std::round始终可以正常工作。对于其他浮点方案,请查阅文档。

但是将0.5加到double中可能会引入不精确性,并导致某些值略微低估或高估。如果您考虑将两个double值相加 - 这是无意识的十进制转换的开始,并应用一个非常强的输入函数(例如舍入函数),那么一定会以泪水结束。

不要这样做

参考文献:为什么Math.round(0.49999999999999994)返回1?


2
评论不适合进行长时间的讨论;此对话已被移至聊天室 - Andy
2
nearbyint()通常比round()更好,因为nearbyint()使用当前的舍入模式,而不是round()的奇怪的远离零的绑定(x86甚至没有硬件支持,尽管ARM有)。两者都在C++11中添加。 - Peter Cordes

10

我认为这就是你犯的错误:

 

但最终它们只是不同的结果,一个选择了它偏爱的结果。我看到很多“老旧”的C/C++程序使用floor(0.5+input)而不是round(input)。

事实并非如此。您必须为领域选择正确的舍入方案。在金融应用中,您将使用银行家规则进行舍入(顺便说一下,不使用float)。然而,在采样时,使用static_cast<int>(floor(f + .5))向上舍入会产生更少的采样噪声,从而增加了动态范围。当对齐像素时,即将位置转换为屏幕坐标时,使用任何其他舍入方法都会产生孔洞、缝隙和其他伪影。


这会增加动态范围。建议删除看起来无意义的额外文本,可能是误复制粘贴。 - anatolyg
不。减少采样噪声会降低噪声底噪,从而确实增加动态范围。 - Michaël Roy
你能提供一个简单的例子或参考来说明动态范围增加/噪声减少吗? - Ruslan
2
使用标准整数(缺乏)舍入时,介于零和负一之间的所有值都会“消失”,或者说改变符号(对于0到+1输入的相同值),所有负值都会偏移至少一个位。这里加入了一点噪音和扭曲。 - Michaël Roy
在一本音频编程书中发现了使用(int)(value + 0.5)代替floor的类似事情。我认为这对我的应用程序有意义,但这是否可以被证明/是否有一个快速阅读来解释为什么这样做是正确的? - SeedyROM
1
@Seedy 使用 floor 函数可以正确地将负数四舍五入。例如:floor(-1.1) == -2.0,而 (int)-1.1 == -1。如果不使用 floor 函数,则会使负数的四舍五入偏向于零。 - Michaël Roy

5
一个简单的原因可能是有不同的四舍五入方法,所以除非你知道使用的方法,否则你可能会得到不同的结果。
使用floor()函数,您可以始终得到一致的结果。如果float为.5或更大,则将其添加将使其上升到下一个int。但是.49999将只是舍去小数部分。

这个回答的观点正是由问题的评论所表达出来的,其中存在关于round()函数的作用有分歧。 - Loduwijk
@Aaron,关于floor(x + 0.5)的作用,答案是错误的。 - EOF
1
@Aaron 很简单。你只需要使用 nearbyint(x),它使用健全的(到最近的偶数)舍入方式,前提是你没有干扰浮点环境。 - EOF
@EOF:是的,我不是银行家。我使用“常规四舍五入”。 - Eric Towers
@EricTowers,我希望你不介意这个系统误差 - 我知道我介意。 - EOF
显示剩余5条评论

2
许多程序员会采用在使用其他语言编程时学到的习惯用语。并非所有语言都有 round() 函数,在这些语言中,通常使用 floor(x + 0.5) 作为替代品。当这些程序员开始使用 C++ 时,他们并不总是意识到有一个内置的 round(),他们继续使用他们习惯的风格。
换句话说,仅仅因为你看到很多代码做某事,并不意味着有充分的理由去这样做。你可以在每种编程语言中找到这种例子。请记住斯特金定律

百分之九十的东西都是垃圾


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接