C++中用于浮点数取整的round()函数

254

我需要一个简单的浮点数舍入函数,例如:

double round(double);

round(0.1) = 0
round(-0.1) = 0
round(-0.9) = -1

我可以在math.h中找到ceil()floor()函数,但是没有找到round()函数。

它是否以另一个名称存在于标准C++库中,还是缺失了?


2
如果你只想将数字输出为一个四舍五入的数,你可以像这样做:std::cout << std::fixed << std::setprecision(0) << -0.9 - Frank
52
保护这个...有着新奇的取整方案的新用户应该先阅读现有的答案。 - Shog9
13
自 C++11 起,round 函数可以在 <cmath> 头文件中使用。不幸的是,如果你在 Microsoft Visual Studio 中使用,该函数仍然缺失:https://connect.microsoft.com/VisualStudio/feedback/details/775474/missing-round-functions-in-standard-library。 - Alessandro Jacopson
3
如我在答案中所述,自己编写“round”函数有很多注意事项。在C++11之前,标准依赖于C90,该版本不包括“round”函数。 C++11依赖于C99,它具有“round”函数,但是正如我所指出的那样,它还包括“trunc”函数,其具有不同的属性,并且根据应用程序可能更适合使用。大多数答案似乎也忽略了用户可能希望返回一个整数类型,这会带来更多问题。 - Shafik Yaghmour
3
@uvts_cvs,这似乎不是最新版本的Visual Studio的问题,点击此处查看演示 - Shafik Yaghmour
显示剩余2条评论
23个回答

149

编辑注:以下答案提供了一个简单的解决方案,但包含多个实现缺陷(请参阅Shafik Yaghmour's answer获取完整说明)。请注意,C++11已经将std::roundstd::lroundstd::llround作为内置函数包含在标准库中。

C++98标准库中没有round()函数。但你可以自己编写一个。以下是一个round-half-up的实现:

double round(double d)
{
  return floor(d + 0.5);
}

C++98标准库中没有round函数的可能原因是它实际上可以用不同的方法实现。上面提到的是一种常见的方法,但还有其他方法,比如round-to-even,这种方法偏差较小,如果你要进行大量舍入,通常更好;但实现起来会稍微复杂一些。

54
这段代码不能正确处理负数。litb的答案是正确的。 - Registered User
40
@InnerJoin:是的,它处理负数的方式与litb的回答不同,但这并不意味着它是“错误的”。 - Roddy
41
在将数值截断前加上0.5会导致多个输入无法四舍五入至最接近的整数,包括0.49999999999999994。详见http://blog.frama-c.com/index.php?post/2013/05/02/nearbyintf1。 - Pascal Cuoq
11
@Sergi0:没有所谓的“正确”和“错误”,因为有多种舍入定义决定了在中间值时会发生什么。在做出判断之前,请核实你的事实。 - Jon
21
@MuhammadAnnaqeeb:你说得没错,自C++11发布以来,情况已经有了极大的改善。这个问题是在另一个时代提出并回答的,那时生活很艰难,乐趣很少。它作为对当时生活和奋斗的英雄们的颂歌留在这里,也为那些仍然无法使用现代工具的可怜人提供帮助。 - Andreas Magnusson
显示剩余9条评论

100
The C++03标准依赖于C90标准,称之为“Standard C Library”,该标准已在C++03标准草案(最接近C++03的公开可用草案是N1804)第1.2节“规范参考”中进行了介绍。
引用如下:

ISO/IEC 9899:1990和ISO/IEC 9899/Amd.1:1995第7条所描述的库以下简称标准C库。1)

如果我们查看cppreference上关于round、lround、llround的C文档,我们可以看到,round和相关函数属于C99,因此在C++03或之前的版本中不可用。
在C++11中,这一点发生了变化,因为C++11依赖于C99草案标准的C标准库,并因此提供std::round和对于整数返回类型的std::lround、std::llround:
#include <iostream>
#include <cmath>

int main()
{
    std::cout << std::round( 0.4 ) << " " << std::lround( 0.4 ) << " " << std::llround( 0.4 ) << std::endl ;
    std::cout << std::round( 0.5 ) << " " << std::lround( 0.5 ) << " " << std::llround( 0.5 ) << std::endl ;
    std::cout << std::round( 0.6 ) << " " << std::lround( 0.6 ) << " " << std::llround( 0.6 ) << std::endl ;
}

另一个来自C99的选项是std::trunc,它会计算不大于参数绝对值的最近整数。
#include <iostream>
#include <cmath>

int main()
{
    std::cout << std::trunc( 0.4 ) << std::endl ;
    std::cout << std::trunc( 0.9 ) << std::endl ;
    std::cout << std::trunc( 1.1 ) << std::endl ;
    
}

如果您需要支持非C++11应用程序,则最好使用boost round,iround,lround,llroundboost trunc自己编写round函数很困难

我建议您拒绝自己编写相关内容,因为难度超乎想象:四舍五入浮点数到最接近的整数,第1部分, 四舍五入浮点数到最接近的整数,第2部分四舍五入浮点数到最接近的整数,第3部分都有所解释:

例如,常见的使用std::floor并添加0.5的实现并不能适用于所有输入:

double myround(double d)
{
  return std::floor(d + 0.5);
}

其中一个会失败的输入是0.49999999999999994(在此查看实时演示)。

另一种常见的实现涉及将浮点类型转换为整数类型,如果整数部分无法在目标类型中表示,则可能引发未定义的行为。我们可以从C++标准草案第4.9浮点-整数转换中看到这一点,该节说道(我强调):

浮点类型的prvalue可以转换为整数类型的prvalue。 转换截断; 也就是说,小数部分被丢弃。如果截断的值不能在目标类型中表示,则行为未定义。[...]

例如:

float myround(float f)
{
  return static_cast<float>( static_cast<unsigned int>( f ) ) ;
}

假设std::numeric_limits<unsigned int>::max()的值为4294967295,则下列调用:

myround( 4294967296.5f ) 

会导致溢出,(在此查看实际效果)。

通过查看在C中实现round()的简洁方法的答案,我们可以看到这真的很困难,它引用了newlibs版本的单精度浮点数round函数。对于似乎很简单的东西来说,这是一个非常长的函数。似乎没有人除了对浮点数实现有深入了解的人能够正确地实现这个函数:

float roundf(x)
{
  int signbit;
  __uint32_t w;
  /* Most significant word, least significant word. */
  int exponent_less_127;

  GET_FLOAT_WORD(w, x);

  /* Extract sign bit. */
  signbit = w & 0x80000000;

  /* Extract exponent field. */
  exponent_less_127 = (int)((w & 0x7f800000) >> 23) - 127;

  if (exponent_less_127 < 23)
    {
      if (exponent_less_127 < 0)
        {
          w &= 0x80000000;
          if (exponent_less_127 == -1)
            /* Result is +1.0 or -1.0. */
            w |= ((__uint32_t)127 << 23);
        }
      else
        {
          unsigned int exponent_mask = 0x007fffff >> exponent_less_127;
          if ((w & exponent_mask) == 0)
            /* x has an integral value. */
            return x;

          w += 0x00400000 >> exponent_less_127;
          w &= ~exponent_mask;
        }
    }
  else
    {
      if (exponent_less_127 == 128)
        /* x is NaN or infinite. */
        return x + x;
      else
        return x;
    }
  SET_FLOAT_WORD(x, w);
  return x;
}

另一方面,如果其他解决方案都不能使用,newlib 可能是一个选择,因为它是一个经过充分测试的实现。

5
@downvoter请解释一下哪里需要改进?这里的大部分答案都是错误的,因为它们尝试自己编写四舍五入函数,但在某种程度上都失败了。如果我的解释有缺失,请告诉我。 - Shafik Yaghmour
1
很好的完整回答 - 特别是下面0.5以下的部分。另一个细节:round(-0.0)。C规范似乎没有指定。我期望结果为-0.0 - chux - Reinstate Monica
3
IEEE 754-2008标准确实规定了四舍五入保留零和无穷大的符号(见5.9节)。 - Ruslan
1
@Shafik 这是一个很棒的答案。我从未想过即使是四舍五入也是一个非平凡的操作。 - Ruslan
1
或许值得一提的是,当C++11可用时,std::rint()在数字和性能方面通常优于std::round()。它使用当前的舍入模式,而不像round()的特殊模式。在x86上,它可以更加高效,其中rint可以内联为单个指令。(即使没有-ffast-math,gcc和clang也会这样做https://godbolt.org/g/5UsL2e,而只有clang内联了几乎相同的`nearbyint()`) ARM支持round()的单指令,但在x86上,它只能通过多个指令进行内联,并且只能使用-ffast-math - Peter Cordes
还可以通过这条推文查看这个全整数实现 - Shafik Yaghmour

96
Boost提供了一组简单的舍入函数。
#include <boost/math/special_functions/round.hpp>

double a = boost::math::round(1.5); // Yields 2.0
int b = boost::math::iround(1.5); // Yields 2 as an integer

想要了解更多信息,请参考Boost documentation

编辑:自 C++11 开始,提供了std::roundstd::lroundstd::llround


2
我已经在我的项目中使用了boost,这个方法比使用天真的floor(value + 0.5)方法好多了!+1 - Gustavo Maciel
@GustavoMaciel 我知道我有点晚了,但是boost的实现是 floor(value + 0.5) - n. m.
实际上并不是这样的:https://github.com/boostorg/math/blob/develop/include/boost/math/special_functions/round.hpp四年后,我也想说floor(value + 0.5)并不幼稚,而是取决于你想要舍入的值的上下文和性质! - Gustavo Maciel

73

值得注意的是,如果你想要一个整数结果,你不需要将它通过 ceil 或 floor 函数取整。也就是说:

int round_int( double r ) {
    return (r > 0.0) ? (r + 0.5) : (r - 0.5); 
}

4
对于0.49999999999999994并没有得到预期的结果(当然,这取决于你的预期是什么,但对我而言,0似乎比1更合理)。 - stijn
@stijn 很好的发现。我发现将长双精度字面量后缀添加到我的常量中可以解决你的示例问题,但我不知道是否有其他精度示例它无法捕获。 - kalaxy
1
顺便说一下,如果你加上0.49999999999999994而不是0.5,它对于输入的0.49999999999999994和5000000000000001.0都可以正常工作。不确定是否适用于所有值,我找不到任何参考资料说明这是最终修复方法。 - stijn
2
@stijn 如果您不关心在两个整数之间的值向哪个方向舍入,那么所有值都可以接受。我会通过以下几种情况来证明它:0 <= d < 0.5, 0.5 <= d < 1.5, 1.5 <= d < 2^52, d >= 2^52。我还对单精度情况进行了详尽的测试。 - Pascal Cuoq
3
根据4.9 [conv.fpint],如果截断后的值不能在目标类型中表示,则其行为未定义,因此这样做有一定的风险。其他SO答案描述了如何进行稳健的操作。 - Tony Delroy
显示剩余2条评论

50

这个功能自C++11起就可以在cmath中使用(根据http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf)。

#include <cmath>
#include <iostream>

int main(int argc, char** argv) {
  std::cout << "round(0.5):\t" << round(0.5) << std::endl;
  std::cout << "round(-0.5):\t" << round(-0.5) << std::endl;
  std::cout << "round(1.4):\t" << round(1.4) << std::endl;
  std::cout << "round(-1.4):\t" << round(-1.4) << std::endl;
  std::cout << "round(1.6):\t" << round(1.6) << std::endl;
  std::cout << "round(-1.6):\t" << round(-1.6) << std::endl;
  return 0;
}

输出:

round(0.5):  1
round(-0.5): -1
round(1.4):  1
round(-1.4): -1
round(1.6):  2
round(-1.6): -2

2
还有lroundllround可用于整数结果。 - sp2danny
@sp2danny: 或者更好的是,使用lrint以使用当前的舍入模式,而不是round的奇怪的远离零的决策。 - Peter Cordes
但是它是如何链接的? - eri0o

27

这通常被实现为 floor(value + 0.5)

编辑:由于至少有三种取整算法:向零取整、四舍五入和银行家舍入,因此它可能不被称为round。你正在寻找的是四舍五入到最接近的整数。


1
将“round”的不同版本区分开来是有好处的。同时,知道什么时候选择哪个版本也是很重要的。 - xtofl
7
确实有不同的舍入算法都可以合理地声称是“正确”的。然而,使用 floor(value + 0.5) 并不属于其中之一。对于某些值,例如 0.49999997f 或其等效的 double 类型,答案是错误的——它将被舍入为 1.0,而所有人都同意应该舍入为零。详情请参见此文章:http://blog.frama-c.com/index.php?post/2013/05/02/nearbyintf1 - Bruce Dawson

14

我们正在考虑两个问题:

  1. 四舍五入转换
  2. 类型转换。

四舍五入转换意味着将±浮点数/双精度浮点数舍入到最近的地板/天花板浮点数/双精度浮点数。 也许你的问题就在这里解决了。 但是如果您需要返回Int/Long,您需要执行类型转换,因此“溢出”问题可能会影响您的解决方案。因此,请检查您的函数是否存在错误。

long round(double x) {
   assert(x >= LONG_MIN-0.5);
   assert(x <= LONG_MAX+0.5);
   if (x >= 0)
      return (long) (x+0.5);
   return (long) (x-0.5);
}

#define round(x) ((x) < LONG_MIN-0.5 || (x) > LONG_MAX+0.5 ?\
      error() : ((x)>=0?(long)((x)+0.5):(long)((x)-0.5))

来源: http://www.cs.tut.fi/~jkorpela/round.html


使用 LONG_MIN-0.5LONG_MAX+0.5 会引入复杂性,因为数学计算可能不精确。LONG_MAX 可能超出了 double 精度的精确转换范围。进一步地,可能需要 assert(x < LONG_MAX+0.5);(使用 < 而非 <=),因为 LONG_MAX+0.5 可能是可精确表示的,而 (x)+0.5 可能具有精确结果 LONG_MAX+1,这将导致 long 强制转换失败。还有其他一些边角问题。 - chux - Reinstate Monica
不要将您的函数命名为 round(double),因为在标准的 math 库中已经有同名的函数(在 C++11 中),这会造成混淆。如果可用,应使用 std::lrint(x) - Peter Cordes

11
Boost库也实现了某种类型的四舍五入:
#include <iostream>

#include <boost/numeric/conversion/converter.hpp>

template<typename T, typename S> T round2(const S& x) {
  typedef boost::numeric::conversion_traits<T, S> Traits;
  typedef boost::numeric::def_overflow_handler OverflowHandler;
  typedef boost::numeric::RoundEven<typename Traits::source_type> Rounder;
  typedef boost::numeric::converter<T, S, Traits, OverflowHandler, Rounder> Converter;
  return Converter::convert(x);
}

int main() {
  std::cout << round2<int, double>(0.1) << ' ' << round2<int, double>(-0.1) << ' ' << round2<int, double>(-0.9) << std::endl;
}

请注意,这仅在进行整数转换时有效。

2
Boost 还提供了一组简单的舍入函数;请参见我的回答。 - Daniel Wolf
如果您不想转换为整数,也可以直接使用 boost:numeric::RoundEven< double >::nearbyint。请注意,简单函数是使用 +0.5 实现的,这可能会出现 aka.nice 所述的问题。 - stijn

7
现在使用包含C99/C++11数学库的C++11编译器不应该成为问题。但问题是:你该选择哪个取整函数?
C99/C++11中的round()通常并不是你想要的取整函数。它使用一种奇怪的取整模式,在半数情况(+-xxx.5000)下向远离0的方向取整。如果你确实需要这种取整模式,或者你正在针对一个比rint()更快的C++实现,则可以使用它(或使用其他答案之一来仔细复制这种特定的取整行为)。
round()的取整方式与IEEE754默认的“就近舍入到最接近的偶数”的方式不同。就近舍入到最接近的偶数避免了数字平均幅度的统计偏差,但会对偶数产生偏见。
有两个数学库取整函数使用当前默认的取整模式:C99/C++11中添加的std::nearbyint()和std::rint(),因此它们在任何时候都可用std::round()。唯一的区别是nearbyint永远不会引发FE_INEXACT。
出于性能原因,请优先考虑使用rint():gcc和clang都更容易内联它,但是gcc甚至不会内联nearbyint()(即使使用-ffast-math)。

x86-64和AArch64的gcc/clang

I put some test functions on Matt Godbolt's Compiler Explorer, where you can see source + asm output (for multiple compilers). For more about reading compiler output, see this Q&A, and Matt's CppCon2017 talk: “What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”.
在FP代码中,将小函数内联通常是一个很大的优势。特别是在非Windows系统上,标准调用约定没有保留寄存器,所以编译器不能在call之间保持任何FP值在XMM寄存器中。因此,即使您不真正了解汇编语言,您仍然可以轻松地看出它是否只是一个尾调用库函数,还是内联到一个或两个数学指令。任何内联到一个或两个指令的内容都比函数调用更好(对于x86或ARM上的这个特定任务)。
在x86上,任何内联到SSE4.1 roundsd的内容都可以自动向量化为SSE4.1 roundpd(或AVX vroundpd)。 (FP->整数转换也可以以打包SIMD形式使用,除了FP-> 64位整数需要AVX512。)
  • std::nearbyint():

    • x86 clang:在使用-msse4.1时可以内联到一个指令。
    • x86 gcc:只有在使用-msse4.1 -ffast-math且gcc版本为5.4或更早版本时才能内联到一个指令。(后来的gcc从未内联过它,也许他们没有意识到其中一个即时位可以抑制不精确例外?这是clang使用的方法,但旧版gcc在内联时使用与rint相同的立即数)
    • AArch64 gcc6.3:默认情况下可以内联到一个指令。
  • std::rint:

    • x86 clang:在使用-msse4.1时可以内联到一个指令。
    • x86 gcc7:在使用-msse4.1时可以内联到一个指令。(如果没有SSE4.1,则内联到几条指令)
    • x86 gcc6.x及更早版本:在使用-ffast-math -msse4.1时可以内联到一个指令。
    • AArch64 gcc:默认情况下可以内联到一个指令。
  • std::round:

    • x86 clang:不会内联。
    • x86 gcc:在使用-ffast-math -msse4.1时,可以内联到多个指令,需要两个矢量常数。
    • AArch64 gcc:默认情况下可内联为一个指令(硬件支持此舍入模式以及IEEE默认值和大部分其他值)。
  • std::floor/std::ceil/std::trunc

    • x86 clang:在使用-msse4.1时可以内联到一个指令。
    • x86 gcc7.x:在使用-msse4.1时可以内联到一个指令。
    • x86 gcc6.x和更早版本:在使用-ffast-math -msse4.1时可以内联到一个指令。
    • AArch64 gcc:默认情况下可以内联到一个指令。

四舍五入到int / long / long long:

您有两种选择:使用lrint(类似于rint,但返回longlong long用于llrint),或使用FP->FP 转换函数,然后以正常方式将其转换为整数类型(通过截断)。一些编译器比另一些更好地优化其中的一种方法。

long l = lrint(x);

int  i = (int)rint(x);

请注意,int i = lrint(x)floatdouble 转换为 long,然后将整数截断为 int。这对于超出范围的整数有所区别:在C++中未定义行为,但对于x86 FP-> int指令是明确定义的(编译器会发出这些指令,除非它在编译时看到UB并进行常量传播,然后它可以生成在执行时会出错的代码)。
在x86上,FP->整数转换溢出整数会产生 INT_MINLLONG_MIN(一个位模式为 0x8000000 或其64位等效项,只有符号位被设置)。英特尔将此称为“整数不定值”。 (请参见 cvttsd2si手动输入,SSE2指令将标量双精度转换为有符号整数(截断)。它可用于32位或64位整数目标(仅在64位模式下)。还有一个cvtsd2si(使用当前舍入模式转换),这是我们希望编译器发出的,但不幸的是,gcc和clang不会在没有-ffast-math的情况下这样做。
还要注意,在x86上,FP与unsigned int / long之间的转换(没有AVX512)效率较低。在64位机器上将其转换为32位无符号整数相当便宜;只需将其转换为64位有符号整数并截断即可。但是,否则它会明显变慢。
  • 带/不带-ffast-math -msse4.1的x86 clang编译器:(int/long)rint 内联到roundsd / cvttsd2sicvtsd2si的优化被忽略了)。lrint完全没有内联。

  • 没有-ffast-math的x86 gcc6.x及之前版本:两种方式都不会内联。

  • x86 gcc7没有-ffast-math(int/long)rint分别进行舍入和转换(如果启用了2个SSE4.1总指令,则为2个总指令,否则会对rint进行大量代码内联而没有roundsd)。lrint 不内联。
  • x86 gcc 使用 -ffast-math:所有方式都内联到cvtsd2si(最佳),不需要SSE4.1。

  • 没有-ffast-math的AArch64 gcc6.3: (int/long)rint 内联到2个指令。 lrint不内联。

  • AArch64 gcc6.3使用-ffast-math(int/long)rint编译为调用lrintlrint不内联。这可能是一个错过的优化,除非我们没有-ffast-math时得到的两个指令非常慢。

TODO: 在Godbolt上也可以使用ICC和MSVC,但我还没有查看它们的输出。欢迎编辑... 另外:按编译器/版本首先进行拆分,然后再按该函数进行拆分,这样是否更有用?大多数人不会根据编译FP-FP或FP-整数舍入的情况切换编译器。 - Peter Cordes
2
在可行的情况下,推荐使用rint(),这通常是一个不错的选择。我猜round()的名称可能会让一些程序员认为这就是他们想要的,而rint()似乎有些神秘。请注意,round()并没有使用“奇怪”的舍入模式:四舍五入到最近的偶数是官方IEEE-754(2008)的舍入模式。有趣的是,nearbyint()没有被内联,尽管它与rint()基本相同,并且在-ffast-math条件下应该是相同的。这看起来像是一个错误。 - njuffa

7

您可以使用以下方法将数字保留n位小数:

double round( double x )
{
const double sd = 1000; //for accuracy to 3 decimal places
return int(x*sd + (x<0? -0.5 : 0.5))/sd;
}

4
除非你的编译器int大小默认为1024位,否则对于巨大的double来说,这不会是准确的。 - aka.nice
我认为根据使用情况,这是可以接受的:如果您的双精度值为1.0e+19,则将其舍入到3位没有意义。 - Carl
3
可以,但问题是通用的,你无法控制使用方式。在没有 ceil 和 floor 会失败的情况下,也没有 round 失败的理由。 - aka.nice
对于超出 int 范围的参数,这将产生未定义行为。(在 x86 上实际上,超出范围的 FP 值 将使 CVTTSD2SI 生成 0x80000000 作为整数位模式,即 INT_MIN,然后将被转换回 double - Peter Cordes

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