C/C++ 中是不是有标准的符号函数(signum、sgn)?

Posted

技术标签:

【中文标题】C/C++ 中是不是有标准的符号函数(signum、sgn)?【英文标题】:Is there a standard sign function (signum, sgn) in C/C++?C/C++ 中是否有标准的符号函数(signum、sgn)? 【发布时间】:2010-12-26 14:42:53 【问题描述】:

我想要一个函数,它对负数返回 -1,对正数返回 +1。 http://en.wikipedia.org/wiki/Sign_function 我自己写很容易,但它似乎应该在某个标准库中。

编辑:具体来说,我正在寻找一个处理浮点数的函数。

【问题讨论】:

0 应该返回什么? @Craig McQueen;这取决于它是正零还是负零。 @ysth @Craig McQueen,花车也是假的,不是吗? sgn(x) 的 definition 表示如果 x==0 则返回 0。根据IEEE 754,负零和正零应该相等。 @ysth “它取决于正零或负零”。事实上,它没有。 评论晚了,但关于有符号零,另一个合理的选择是当 x 为零时 sgn(x) 返回 x。换句话说,你得到 0,但它是一个有符号的零,与输入的符号相同。 @RJFalconer 在签名零很重要的相对少数情况下,您会得到一个明智的答案,而在其他情况下则没有区别。 【参考方案1】:

类型安全的 C++ 版本:

template <typename T> int sgn(T val) 
    return (T(0) < val) - (val < T(0));

好处:

实际上实现了符号(-1、0 或 1)。此处使用 copysign 的实现仅返回 -1 或 1,这不是符号。此外,这里的一些实现返回一个浮点数(或 T)而不是一个 int,这似乎很浪费。 适用于整数、浮点数、双精度数、无符号短裤或任何可从整数 0 构造且可排序的自定义类型。 快! copysign 很慢,尤其是当您需要提升然后再次缩小时。这是无分支的,并且优化得非常好 符合标准! bitshift hack 很简洁,但仅适用于某些位表示,并且在您具有无符号类型时不起作用。它可以在适当的时候以手动专业化的形式提供。 准确!与零的简单比较可以保持机器内部的高精度表示(例如 x87 上的 80 位),并避免过早舍入为零。

注意事项:

这是一个模板,因此在某些情况下编译可能需要更长的时间。

显然,有些人认为使用一个新的、有点深奥、非常慢的标准库函数甚至没有真正实现 signum 更容易理解。

当为无符号类型实例化时,检查的&lt; 0 部分会触发 GCC 的 -Wtype-limits 警告。您可以通过使用一些重载来避免这种情况:

 template <typename T> inline constexpr
 int signum(T x, std::false_type is_signed) 
     return T(0) < x;
 

 template <typename T> inline constexpr
 int signum(T x, std::true_type is_signed) 
     return (T(0) < x) - (x < T(0));
 

 template <typename T> inline constexpr
 int signum(T x) 
     return signum(x, std::is_signed<T>());
 

(这是第一个警告的一个很好的例子。)

【讨论】:

@GMan:GCC 刚刚(4.5)停止了与模板函数的实例化数量成二次方的成本,而且它们的解析和实例化成本仍然比手动编写的函数或标准 C 高得多预处理器。链接器还必须做更多的工作来删除重复的实例化。模板还鼓励#includes-in-#includes,这使得依赖计算需要更长的时间和更小的(通常是实现,而不是接口)更改来强制重新编译更多文件。 @Joe:是的,而且仍然没有明显的成本。 C++ 使用模板,这是我们都必须理解、接受和克服的东西。 等等,这是什么“copysign 很慢”的业务......?使用当前的编译器(g++ 4.6+,clang++ 3.0),std::copysign 似乎为我生成了 excellent 代码:4 条指令(内联),没有分支,完全使用 FPU。相比之下,这个答案中给出的配方会生成更糟糕的代码(更多的指令,包括乘法,在整数单元和 FPU 之间来回移动)...... @snogglethorpe:如果您在 int 上调用 copysign,它会提升为 float/double,并且必须在返回时再次缩小。您的编译器可能会优化该促销,但我找不到任何表明标准保证的内容。此外,要通过 copysign 实现 signum,您需要手动处理 0 情况 - 请确保将其包含在任何性能比较中。 第一个版本不是无分支的。为什么人们认为表达式中使用的比较不会生成分支?它适用于大多数架构。只有具有 cmove(或谓词)的处理器才会生成无分支代码,但它们也会为三元组或 if/else(如果获胜)生成。【参考方案2】:

我不知道它的标准功能。不过,这是一种有趣的编写方式:

(x > 0) - (x < 0)

这是一种更易读的方法:

if (x > 0) return 1;
if (x < 0) return -1;
return 0;

如果你喜欢三元运算符,你可以这样做:

(x > 0) ? 1 : ((x < 0) ? -1 : 0)

【讨论】:

Mark Ransom,你的表达式给出的x==0 的结果不正确。 @Svante: "每个运算符 &lt;, &gt; ... 如果指定的关系为真,则返回 1,如果为假,则返回 0" @Svante:不完全是。 0 的值为“假”;任何其他值为“真”;但是,关系运算符和相等运算符总是返回 01(参见标准 6.5.8 和 6.5.9)。 -- 表达式a * (x == 42) 的值为0a 高性能标记,我很惊讶你错过了 C++ 标签。这个答案非常有效,不值得投反对票。此外,即使我有可用的copysign,我也不会将其用于积分x 有没有人真正检查过 GCC/G++/任何其他编译器在真实平台上发出的代码?我的猜测是“无分支”版本使用两个分支而不是一个。位移可能更快 - 在性能方面更便携。【参考方案3】:

有一个名为 copysign() 的 C99 数学库函数,它从一个参数中获取符号,从另一个参数中获取绝对值:

result = copysign(1.0, value) // double
result = copysignf(1.0, value) // float
result = copysignl(1.0, value) // long double

将根据值的符号为您提供 +/- 1.0 的结果。请注意,浮点零是有符号的:(+0) 将产生 +1,而 (-0) 将产生 -1。

【讨论】:

赞成这个,反对最流行的答案。令人惊讶的是,SO 社区似乎更喜欢 hack 而不是使用标准库函数。愿编程之神谴责你们所有人试图破译不熟悉语言标准的聪明程序员使用的黑客攻击。是的,我知道这会在 SO 上花费我大量的代表,但我宁愿支持 Cometstorm,而不是你们其他人...... 这很接近,但它给出了零的错误答案(至少根据问题中的***文章)。不错的建议。无论如何 +1。 1) 并非所有地方都完全支持 C99(考虑 VC++); 2)这也是一个 C++ 问题。这是一个很好的答案,但赞成的答案也有效,并且适用范围更广。 我不会在 AVR 微控制器上使用 copysign(),与“黑客”相比,它为程序大小增加了惊人的 334 字节(如果还没有使用来自 math.h 的任何其他内容)。 我通常使用标准库函数,但这确实不能满足要求,因为末尾有关于带符号浮点 0 的注释。如果您的用例真的想要 sgn( 0) 给出 +1 或 -1,那么这是可以的,但我认为大多数寻找 sgn 函数的人会希望它总是给出 0,因为这是通常的数学约定,它与其他语言匹配。跨度> 【参考方案4】:

似乎大多数答案都错过了原始问题。

C/C++中有标准的符号函数(signum, sgn)吗?

标准库中没有,但是copysign 可以通过copysign(1.0, arg) 以几乎相同的方式使用,boost 中有一个真正的符号函数,它也可能是标准的一部分。

    #include <boost/math/special_functions/sign.hpp>

    //Returns 1 if x > 0, -1 if x < 0, and 0 if x is zero.
    template <class T>
    inline int sign (const T& z);

http://www.boost.org/doc/libs/1_47_0/libs/math/doc/sf_and_dist/html/math_toolkit/utils/sign_functions.html

【讨论】:

过去几分钟我一直在想为什么标准库没有签名功能。它是如此常见——绝对比 cmath 标头中的 gamma 函数更常用。 对于类似问题,我经常得到的解释是“实现自己很容易”,IMO 不是一个很好的理由。它完全掩盖了在哪里标准化、不明显的边缘情况以及在哪里放置如此广泛使用的工具的问题。 我不希望将此标记为答案,因为它说要使用外部非标准库。我不使用 Boost,也不能使用 Boost,所以这没有帮助。【参考方案5】:

显然,原始发帖人问题的答案是否定的。没有标准 C++ sgn 函数。

【讨论】:

@SR_ 你不正确。如果第二个参数为 0.0,copysign() 不会使您的第一个参数为 0.0。换句话说,约翰是正确的。【参考方案6】:

C/C++中有标准的符号函数(signum, sgn)吗?

是的,取决于定义。

C99 及更高版本在&lt;math.h&gt; 中有signbit()

int signbit(实-浮动x); 当且仅当其参数值的符号为负时,signbit 宏才会返回非零值。 C11 §7.12.3.6


然而 OP 想要一些不同的东西。

我想要一个函数,它对负数返回 -1,对正数返回 +1。 ... 一个处理浮点数的函数。

#define signbit_p1_or_n1(x)  ((signbit(x) ?  -1 : 1)

更深入:

OP 的问题在以下情况下并不具体:x = 0.0, -0.0, +NaN, -NaN

经典的signum()x&gt;0 上返回+1,在x&lt;0 上返回-1x==0 上返回0

许多答案已经涵盖了这一点,但没有解决x = -0.0, +NaN, -NaN。许多都是针对通常缺乏非数字(NaN)和-0.0的整数观点。

signnum_typical() 这样的典型答案函数在-0.0, +NaN, -NaN 上,它们返回0.0, 0.0, 0.0

int signnum_typical(double x) 
  if (x > 0.0) return 1;
  if (x < 0.0) return -1;
  return 0;

相反,我建议使用此功能:在 -0.0, +NaN, -NaN 上,它返回 -0.0, +NaN, -NaN

double signnum_c(double x) 
  if (x > 0.0) return 1.0;
  if (x < 0.0) return -1.0;
  return x;

【讨论】:

啊,正是我想要的。这只是在 Pharo Smalltalk github.com/pharo-project/pharo/pull/1835 中发生了变化,我想知道是否有某种标准(IEC 60559 或 ISO 10967)规定了负零和 nan 行为的行为......我喜欢 javascript 符号 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…【参考方案7】:

比上述解决方案更快,包括评分最高的解决方案:

(x < 0) ? -1 : (x > 0)

【讨论】:

x 是什么类型?还是您使用的是#define? 您的输入速度并不快。它会经常导致缓存未命中。 缓存未命中?我不确定如何。也许您的意思是分支错误预测? 在我看来这会导致整数和布尔类型混淆的警告! 这个分支的速度如何?【参考方案8】:

有一种方法可以不用分支,但不是很漂亮。

sign = -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1));

http://graphics.stanford.edu/~seander/bithacks.html

该页面上还有很多其他有趣的、过于聪明的东西......

【讨论】:

如果我正确阅读了只返回-1或0的链接。如果你想要-1、0或+1,那么它是sign = (v != 0) | -(int)((unsigned int)((int)v) &gt;&gt; (sizeof(int) * CHAR_BIT - 1));sign = (v &gt; 0) - (v &lt; 0); 这意味着v是一个不比int宽的整数类型【参考方案9】:

如果您只想测试符号,请使用signbit(如果其参数有负号则返回真)。 不知道您为什么特别希望返回 -1 或 +1; copysign 更方便 为此,但听起来它会在某些平台上为负零返回 +1 仅部分支持负零,其中符号位可能会返回 true。

【讨论】:

在许多数学应用中,sign(x) 是必需的。否则我只会做if (x &lt; 0)【参考方案10】:

一般来说,C/C++ 中没有标准的符号函数,缺少这样一个基本函数可以告诉你很多关于这些语言的信息。

除此之外,我相信大多数关于定义此类函数的正确方法的观点在某种程度上都是正确的,一旦您考虑到两个重要的警告,关于它的“争议”实际上是一个非争论:

signum 函数应始终返回其操作数的类型,类似于 abs() 函数,因为 signum 通常用于与绝对值相乘后者以某种方式处理后的值。因此,signum 的主要用例不是比较而是算术,后者不应该涉及任何昂贵的整数到浮点数的转换。

浮点类型不具有单个精确零值:+0.0 可以解释为“无限小于零”,-0.0 可以解释为“无限小于零”。这就是为什么涉及零的比较必须在内部检查两个值的原因,而像 x == 0.0 这样的表达式可能很危险。

关于 C,我认为使用整数类型最好的方法确实是使用 (x &gt; 0) - (x &lt; 0) 表达式,因为它应该以无分支的方式进行翻译,并且只需要三个基本操作。最好定义强制返回类型与参数类型匹配的内联函数,并添加 C11 define _Generic 以将这些函数映射到一个通用名称。

对于浮点值,我认为基于 C11 copysignf(1.0f, x)copysign(1.0, x)copysignl(1.0l, x) 的内联函数是可行的方法,因为它们也很可能是无分支的,而且还可以不需要将整数的结果转换回浮点值。您可能应该强调指出,您的 signum 浮点实现不会返回零,因为浮点零值的特殊性、处理时间的考虑以及因为它在浮点运算中通常非常有用接收正确的 -1/+1 符号,即使是零值。

【讨论】:

【参考方案11】:

我的 C in a Nutshell 副本揭示了一个名为 copysign 的标准函数的存在,它可能很有用。看起来 copysign(1.0, -2.0) 会返回 -1.0,而 copysign(1.0, 2.0) 会返回 +1.0。

很接近吧?

【讨论】:

不是标准的,但可以广泛使用。 Microsoft 以下划线开头,这是他们用于非标准扩展的约定。但是,当您使用整数时,这不是最佳选择。 copysign 符合 ISO C (C99) 和 POSIX 标准。见opengroup.org/onlinepubs/000095399/functions/copysign.html lhf 所说的。 Visual Studio 不是 C 标准的参考。【参考方案12】:

以下重载的接受答案确实不会触发-Wtype-limits。但它确实会触发 unused argument 警告(在 is_signed 变量上)。为了避免这些,第二个参数不应该这样命名:

template <typename T> inline constexpr
  int signum(T x, std::false_type) 
  return T(0) < x;


template <typename T> inline constexpr
  int signum(T x, std::true_type) 
  return (T(0) < x) - (x < T(0));


template <typename T> inline constexpr
  int signum(T x) 
  return signum(x, std::is_signed<T>());

对于 C++11 及更高版本,可能是替代方案。

template <typename T>
typename std::enable_if<std::is_unsigned<T>::value, int>::type
inline constexpr signum(T const x) 
    return T(0) < x;  


template <typename T>
typename std::enable_if<std::is_signed<T>::value, int>::type
inline constexpr signum(T const x) 
    return (T(0) < x) - (x < T(0));  

对我来说,它不会在 GCC 5.3.1 上触发任何警告。

【讨论】:

要避免-Wunused-parameter 警告,只需使用未命名的参数。 确实如此。我错过了。但是,无论哪种方式,我都更喜欢 C++11 替代方案。【参考方案13】:

不,它在 c++ 中不存在,就像在 matlab 中一样。为此,我在我的程序中使用了一个宏。

#define sign(a) ( ( (a) < 0 )  ?  -1   : ( (a) > 0 ) )

【讨论】:

在 C++ 中应该更喜欢模板而不是宏。 在C中,没有模板......helloacm.com/how-to-implement-the-sgn-function-in-c 我认为这是一个很好的答案,然后我查看了自己的代码,发现:#define sign(x) (((x) &gt; 0) - ((x) &lt; 0)) 这也很好。 在 C 中内联函数比宏好,在 C++ 模板中更好【参考方案14】:

这个问题很老,但现在有这种想要的功能。我添加了一个带有 not、left shift 和 dec 的包装器。

您可以使用基于 signbit from C99 的包装函数来获得所需的确切行为(请参阅下面的代码)。

返回 x 的符号是否为负。 这也可以应用于无穷大、NaN 和零(如果零是无符号的,则认为它是正数

#include <math.h>

int signValue(float a) 
    return ((!signbit(a)) << 1) - 1;

注意:我使用操作数 not ("!") 因为符号位的返回值未指定为 1(尽管示例让我们认为它总是这样)但对于负数为 true:

返回值 如果 x 的符号为负,则为非零值(真);否则为零(假)。

然后我用左移乘以 2 ("

【讨论】:

0 也会是积极的......这可能是也可能不是 OP 想要的...... 好吧,如果 n=0,我们可能永远不会知道 OP 真正想要什么...!【参考方案15】:

有点跑题了,但我用这个:

template<typename T>
constexpr int sgn(const T &a, const T &b) noexcept
    return (a > b) - (a < b);


template<typename T>
constexpr int sgn(const T &a) noexcept
    return sgn(a, T(0));

我发现第一个函数 - 有两个参数的函数,在“标准”sgn() 中更有用,因为它最常用于如下代码中:

int comp(unsigned a, unsigned b)
   return sgn( int(a) - int(b) );

对比

int comp(unsigned a, unsigned b)
   return sgn(a, b);

无符号类型没有强制转换,也没有额外的减号。

事实上我有这段代码使用 sgn()

template <class T>
int comp(const T &a, const T &b)
    log__("all");
    if (a < b)
        return -1;

    if (a > b)
        return +1;

    return 0;


inline int comp(int const a, int const b)
    log__("int");
    return a - b;


inline int comp(long int const a, long int const b)
    log__("long");
    return sgn(a, b);

【讨论】:

【参考方案16】:

如果 boost 可用,您可以使用 boost/math/special_functions/sign.hpp 中的 boost::math::sign() 方法。

【讨论】:

请注意,这是之前建议的:***.com/a/16869019/1187415. Boost 不是标准库,我们中的一些人不允许在我们的项目中使用 Boost。【参考方案17】:

虽然接受答案中的整数解决方案非常优雅,但它无法为双精度类型返回 NAN,这让我很困扰,所以我稍微修改了它。

template <typename T> double sgn(T val) 
    return double((T(0) < val) - (val < T(0)))/(val == val);

请注意,返回浮点 NAN 而不是硬编码的NAN 会导致在some implementations 中设置符号位,因此val = -NANval = NAN 的输出无论如何都将是相同的(如果您更喜欢“nan”输出而不是 -nan,则可以在返回前添加 abs(val)...)

【讨论】:

【参考方案18】:

这是一个分支友好的实现:

inline int signum(const double x) 
    if(x == 0) return 0;
    return (1 - (static_cast<int>((*reinterpret_cast<const uint64_t*>(&x)) >> 63) << 1));

除非您的数据有一半的数字为零,否则分支预测器将选择其中一个作为最常见的分支。两个分支都只涉及简单的操作。

或者,在某些编译器和 CPU 架构上,完全无分支的版本可能更快:

inline int signum(const double x) 
    return (x != 0) * 
        (1 - (static_cast<int>((*reinterpret_cast<const uint64_t*>(&x)) >> 63) << 1));

这适用于IEEE 754 double-precision binary floating-point format: binary64。

【讨论】:

【参考方案19】:
int sign(float n)
     
  union  float f; std::uint32_t i;  u  n ;
  return 1 - ((u.i >> 31) << 1);

这个函数假设:

binary32 浮点数的表示 一个编译器,在使用 named 联合时产生一个关于严格别名的异常 规则

【讨论】:

这里还有一些不好的假设。例如,我不相信浮点数的字节序保证是整数的字节序。您对使用 ILP64 的任何架构的检查也失败了。真的,你只是在重新实现copysign;如果您使用的是static_assert,那么您已经获得了 C++11,并且还不如真正使用 copysign【参考方案20】:
double signof(double a)  return (a == 0) ? 0 : (a<0 ? -1 : 1); 

【讨论】:

【参考方案21】:

既然可以做到这一点,为什么还要使用三元运算符和 if-else

#define sgn(x) x==0 ? 0 : x/abs(x)

【讨论】:

您的定义也使用了三元运算符。 是肯定的,但它只是使用一个三元运算符来分隔零和非零数字。其他版本包括嵌套三元操作来分隔正、负和零。 使用整数除法效率非常低,abs() 仅适用于整数。 x == INT_MIN 时可能出现未定义行为。

以上是关于C/C++ 中是不是有标准的符号函数(signum、sgn)?的主要内容,如果未能解决你的问题,请参考以下文章

为什么C或C ++标准没有明确地将char定义为signed或unsigned?

JDK源码之Integer类—signum()方法

在C语言中,unsigned char是啥类型

C语言中时间的函数

c size_t

2021-09-12:请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。函数 myAtoi(string(