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


409

我想要一个返回负数为-1和正数为+1的函数。 http://en.wikipedia.org/wiki/Sign_function 编写我自己的代码很容易,但是似乎应该将其存储在标准库中。

编辑:具体来说,我正在寻找一个对浮点数起作用的函数。


13
0应该返回什么?
Craig McQueen

61
@克雷格·麦奎因; 这取决于它是正零还是负零。
ysth

1
我注意到您将返回值指定为整数。您是否正在寻找一个采用整数或浮点数的解决方案?
Mark Byers

6
@ysth @Craig McQueen,对于花车也是错误的,不是吗?sgn(x)的定义说,如果返回,则返回0 x==0。根据IEEE 754,负零和正零应比较相等。
RJFalconer 2014年

5
@ysth“它取决于正零或负零”。实际上,事实并非如此。
RJFalconer 2014年

Answers:


506

惊讶的是没有人发布过类型安全的C ++版本:

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

优点:

  • 实际实现信号(-1、0或1)。此处使用copysign的实现仅返回-1或1,而不是signum。另外,这里的某些实现返回的是float(或T)而不是int,这似乎很浪费。
  • 适用于int,float,double,unsigned short或任何可从整数0构造且可排序的自定义类型。
  • 快速!copysign是缓慢的,特别是如果您需要提升然后再次缩小。这是无分支的,并且进行了优化
  • 符合标准!位移位技巧很简洁,但仅适用于某些位表示形式,当您使用无符号类型时不起作用。适当时,可以将其作为手动专业提供。
  • 准确!简单的零比较可以保持机器内部的高精度表示(例如x87上的80位),并避免过早舍入为零。

注意事项:

  • 这是一个模板,因此在某些情况下可能需要更长的时间进行编译。
  • 显然,有些人认为使用新的,有些深奥的,非常慢的标准库函数(甚至没有真正实现signum)更容易理解。
  • 在为无符号类型实例化时< 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>());
    }

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


18
@GMan:GCC直到现在(4.5)才停止使模板函数的实例化成本增加到两倍,而与手动编写的函数或标准C预处理程序相比,它们的解析和实例化仍然要昂贵得多。链接器还必须做更多的工作来删除重复的实例。模板还鼓励#includes-in-#includes,这使得依赖性计算花费的时间更长而又很小(通常是实现,而不是接口),从而迫使更多文件重新编译。

15
@乔:是的,仍然没有可观的成本。C ++使用模板,这只是我们所有人都必须理解,接受和克服的东西。
GManNickG 2011年

42
等等,这是什么“复制签名缓慢”的业务...?使用当前的编译器(g ++ 4.6 +,clang ++ 3.0)std::copysign似乎为我带来了出色的代码:4条指令(内联),无分支,完全使用FPU。相比之下,此答案中给出的配方会生成更差的代码(许多指令,包括乘法,在整数单位和FPU之间来回移动)...
snogglethorpe 2012年

14
@snogglethorpe:如果您正在调用copysign一个int,它将提升为float / double,并且必须在返回时再次变窄。您的编译器可能会优化升级,但是我找不到任何暗示可以保证该标准的内容。另外,要通过copysign实现信号传递,您还需要手动处理0大小写-请确保在任何性能比较中都包括了这种情况。

53
第一个版本不是无分支的。人们为什么认为表达式中使用的比较不会生成分支?它将在大多数体系结构上使用。只有具有cmove(或谓词)的处理器才会生成无分支代码,但它们也将对三元组或if / else(如果获胜)也这样做。
PatrickSchlüter2012年

271

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

(x > 0) - (x < 0)

这是一种更易读的方法:

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

如果您喜欢三元运算符,则可以执行以下操作:

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

7
马克·兰瑟姆(Mark Ransom),您的表达式给出了不正确的结果x==0
avakar

3
@Svante:“每个运算符<>如果指定的关系为真,则...将产生1,如果为假的关系则为0”
Stephen Canon

11
@Svante:不完全是。值为0“ false”;其他任何值为“ true”;但是,关系和相等运算符始终返回01(请参见标准6.5.8和6.5.9)。-表达式的值a * (x == 42)0a
09年

21
高性能标记,令您惊讶的是您错过了C ++标签。这个答案非常有效,不应该被否决。而且,即使有copysign积分,我也不会使用积分x
avakar

6
有没有人真正检查过GCC / G ++ /任何其他编译器在真实平台上发出的代码?我的猜测是“无分支”版本使用两个分支而不是一个。位移位可能更快得多-并且在性能方面更可移植。
约尔根·福格(JørgenFogh)

192

有一个名为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。


57
赞成这个,反对最受欢迎的答案。令惊讶的是,SO社区似乎更喜欢使用骇客技术而不是使用标准库功能。愿编程之神谴责大家试图破译那些不熟悉语言标准的聪明程序员所使用的黑客程序。是的,我知道这将使我在SO上花费大量的代表,但与其他人相比,我宁愿面对即将来临的风暴……
高性能马克

34
这很接近,但是给出的错误答案为零(至少根据问题中的Wikipedia文章)。不错的建议。还是+1。
Mark Byers

4
如果您想要一个整数,或者想要精确的零符号结果,我喜欢Mark Byers的答案,这非常好!如果您不关心上述内容,则copysign()可能会在性能上有所提高,具体取决于应用程序-如果我正在优化关键循环,则可以同时尝试两者。
即将

10
1)并非到处都完全支持C99(考虑VC ++);2)这也是一个C ++问题。这是一个很好的答案,但是被投票赞成的也可以,并且更广泛地适用。
帕维尔米纳夫

5
救主!需要一种方法,-0.0和0.0之间确定
奥拉维尔Waage

79

似乎大多数答案都没有回答原始问题。

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

标准库中没有,但是copysign可以通过几乎相同的方式使用它,copysign(1.0, arg)并且在中有一个真正的sign函数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


5
这应该是投票最多的答案,因为它为问题提出了最接近的解决方案。
BartoszKP'3

在过去的几分钟中,我一直想知道为什么标准库没有符号功能。它是如此常见-绝对比cmath标头中的gamma函数更常用。
桃子

4
对于类似问题,我经常得到的解释是“实施起来很容易”,这不是IMO的充分理由。这完全掩盖了标准化,不明显的边缘情况以及在何处放置如此广泛使用的工具的问题。
Catskul

77

显然,原始海报问题的答案是“否”。没有标准的 C ++ sgn函数。


2
@SR_您不正确。copysign()如果第二个参数为0.0,则不会使第一个参数为0.0。换句话说,约翰是正确的。
Alexis Wilke

29

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

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

1
x是什么类型?还是使用#define?
2012年

3
您的输入速度不是很快。这会经常导致高速缓存未命中。
Jeffrey Drake

19
缓存未命中?我不确定如何。也许您的意思是分支预测错误?
Catskul 2013年

2
在我看来,这将导致整数和布尔类型混淆的警告!
sergiol 2015年

分支将如何快速?
尼克

29

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

是的,取决于定义。

C99及更高版本中包含signbit()<math.h>

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


但是OP希望有所不同。

我想要一个返回负数为-1和正数为+1的函数。...在浮点数上起作用的函数

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

更深层次的:

该文章没有具体在以下情况:x = 0.0, -0.0, +NaN, -NaN

一个经典的signum()回报+1x>0-1x<00x==0

已经有很多答案解决了,但是没有解决x = -0.0, +NaN, -NaN。许多设备都针对整数视角,通常缺少非数字(NaN)和-0.0

典型的答案功能类似于signnum_typical() On -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;
}

1
啊,正是我所追求的。这只是在Pharo Smalltalk中更改了github.com/pharo-project/pharo/pull/1835,我想知道是否存在某种标准(IEC 60559或ISO 10967)来规定负零和nan行为的行为...我喜欢JavaScript的迹象developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
aka.nice

16

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

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

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

该页面上还有许多其他有趣的,过于机灵的内容...


1
如果我正确阅读链接,则仅返回-1或0。如果您想要-1、0或+1,则为sign = (v != 0) | -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1));sign = (v > 0) - (v < 0);
Z玻色子2015年

1
这意味着这v是一个不比int宽的整数类型
phuclv

12

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


7
在许多数学应用中,必须使用sign(x)。否则我会做的if (x < 0)
机遇

5

通常,C / C ++中没有标准的signum函数,并且缺少这样的基本函数会告诉您很多有关这些语言的信息。

除此之外,我相信关于定义这种功能的正确方法的大多数观点都正确,并且一旦考虑到两个重要的警告,关于该功能的“争论”实际上是毫无争议的:

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

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

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

随着浮点值,我觉得内联函数基于C11 copysignf(1.0f, x)copysign(1.0, x)以及copysignl(1.0l, x)是要走的路,只是因为他们也极有可能成为分支免费的,另外不需要从铸造整数回到浮点结果值。您可能应该特别指出,由于浮点零值的特殊性,处理时间的考虑,以及因为在浮点算术中接收正确的-1 / +常常非常有用,因此signum的浮点实现不会返回零。 1个符号,即使对于零值也是如此。


5

我在坚果壳中的C副本揭示了一个叫做copysign的标准函数的存在,该函数可能会有用。看来copysign(1.0,-2.0)将返回-1.0,而copysign(1.0,2.0)将返回+1.0。

差不多吧?


不是标准的,但可能广泛可用。微软以下划线开头,这是它们用于非标准扩展的约定。但是,当您使用整数时,这不是最佳选择。
Mark Ransom

5
copysign符合ISO C(C99)和POSIX标准。参见opengroup.org/onlinepubs/000095399/functions/copysign.html
lhf

3
lhf说了什么。Visual Studio不是C标准的参考。
斯蒂芬·佳能

3

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

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

5
人们应该更喜欢模板而不是C ++中的宏。
Ruslan


我以为这是一个好答案,然后我查看了自己的代码并发现了这一点:#define sign(x) (((x) > 0) - ((x) < 0))这也很好。
米歇尔·鲁齐奇

1
内联函数比C中的宏更好,并且在C ++模板中也更好
phuclv

3

接受的带有以下过载的答案的确不会触发-Wtype-limits

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替代品。
SamVanDonut

2

有点题外话,但是我用这个:

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);
}

1

问题很老,但是现在有这种所需的功能。我添加了一个包装,没有,左移和十二月。

您可以使用基于C99的符号位的包装函数,以获取确切的所需行为(请参见下面的代码)。

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

#include <math.h>

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

注意:我使用的操作数不是(“!”),因为signbit的返回值未指定为1(即使示例让我们认为它总是这样),但对于负数为true:

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

然后,我乘以2并左移(“ << 1”),这将使我们得到2代表正数,0代表负1,最后减1分别得到正负数的1和-1,分别由OP。


0也将是正数...这可能是或不是OP想要的...
Antti Haapala

好吧,如果n = 0 ...,我们可能永远不知道OP真正想要的是什么!
Antonin GAVREL

0

尽管接受的答案中的整数解决方案非常优雅,但令我感到困扰的是,它无法为双精度类型返回NAN,因此我对其进行了一些修改。

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

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



0

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

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双精度二进制浮点格式:binary64


-1
int sign(float n)
{     
  union { float f; std::uint32_t i; } u { n };
  return 1 - ((u.i >> 31) << 1);
}

该函数假定:

  • 浮点数的binary32表示形式
  • 使用命名联合时对严格别名规则进行例外处理的编译器

3
这里仍然有一些错误的假设。例如,我不相信float的字节序可以保证是整数的字节序。在使用ILP64的任何体系结构上,检查也将失败。真的,您只是在重新实现copysign;如果您使用的static_assert是C ++ 11,则最好使用copysign


-3

为什么只需使用三元运算符和if-else即可

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

3
您的定义也使用三元运算符。
Martin R

是的,可以,但是只使用一个三元运算符来分隔零和非零数字。其他版本包括嵌套的三元运算符,以分隔正数,负数和零。
Jagreet

使用整数除法效率很低,而abs()仅适用于整数。
米歇尔·罗齐奇

时可能出现不确定的行为x == INT_MIN
chux-
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.