ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

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

S
Stef

类型安全的 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 更容易理解。

当为无符号类型实例化时,检查的 < 0 部分会触发 GCC 的 -Wtype-limits 警告。您可以通过使用一些重载来避免这种情况: template inline constexpr int signum(T x, std::false_type is_signed) { return T(0) < x; } template inline constexpr int signum(T x, std::true_type is_signed) { return (T(0) < x) - (x < T(0)); } template inline constexpr int signum(T x) { return signum(x, std::is_signed()); (这是第一个警告的一个很好的例子。)


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

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

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

有一个名为 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(),与“hacks”相比,它增加了惊人的 334 字节的程序大小(如果尚未使用 math.h 中的任何其他内容)。
我通常用于使用标准库函数,但这确实不能满足要求,因为末尾有关于带符号浮点 0 的注释。如果您的用例真的希望 sgn(0) 给出 +1 或 - 1,那么这没关系,但我认为大多数寻找 sgn 函数的人都希望它总是给出 0,因为这是通常的数学约定,它与其他语言匹配。
C
Catskul

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

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 函数更常用。
对于类似问题,我经常得到的解释是“自己实现起来很容易”,这不是一个很好的理由。它完全掩盖了标准化、不明显的边缘情况以及在哪里放置如此广泛使用的工具的问题。
我不希望将此标记为答案,因为它说要使用外部非标准库。我不使用 Boost,也不能使用 Boost,所以这没有帮助。
J
John

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


@SR_你不正确。如果第二个参数为 0.0,copysign() 将不会使您的第一个参数为 0.0。换句话说,约翰是正确的。
c
chux - Reinstate Monica

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

是的,取决于定义。

C99 及更高版本在 <math.h> 中具有 signbit()

int 符号位(实浮点 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>0 上返回 +1,在 x<0 上返回 -1,在 x==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/…
T
Tim Cooper

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

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

x是什么类型?还是您使用#define?
你的类型不是更快。它会经常导致缓存未命中。
缓存未命中?我不确定如何。也许您的意思是分支错误预测?
在我看来,这将导致整数和布尔类型混淆的警告!
这将如何快速与分支?
T
Tim Sylvester

有一种无需分支的方法,但它不是很漂亮。

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) >> (sizeof(int) * CHAR_BIT - 1));sign = (v > 0) - (v < 0);
这意味着 v 是不比 int 宽的整数类型
y
ysth

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


在许多数学应用中,sign(x) 是必需的。否则我只会做if (x < 0)
s
seh

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

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

符号函数应始终返回其操作数的类型,类似于 abs() 函数,因为符号通常用于在以某种方式处理绝对值之后与绝对值相乘。因此,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) 的内联函数是可行的方法,因为它们也很可能是无分支的,而且不需要强制转换将整数返回到浮点值的结果。您可能应该强调指出,您的 signum 浮点实现不会返回零,因为浮点零值的特殊性、处理时间的考虑以及因为它在浮点运算中通常非常有用接收正确的 -1/+1 符号,即使是零值。


m
mbaitoff

我的 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 标准的参考。
S
SamVanDonut

下面重载的公认答案确实不会触发 -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 替代方案。
c
chattering

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

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

在 C++ 中,人们应该更喜欢模板而不是宏。
我认为这是一个很好的答案,然后我查看了自己的代码,发现:#define sign(x) (((x) > 0) - ((x) < 0)) 这也很好。
内联函数比 C 中的宏好,而在 C++ 模板中更好
A
Antonin GAVREL

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

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

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

#include <math.h>

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

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

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

然后我用左移乘以 2(“<< 1”),这将给我们 2 表示正数和 0 表示负数,最后减 1 以获得 1 和 -1 分别表示所要求的正数和负数操作。


0 也将是积极的......这可能是也可能不是 OP 想要的......
好吧,如果 n=0,我们可能永远不会知道 OP 真正想要什么......!
N
Nick

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

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

k
khkarens

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


请注意,这是之前建议的:stackoverflow.com/a/16869019/1187415
Boost 不是标准库,我们中的一些人不允许在我们的项目中使用 Boost。
m
mrclng

虽然接受的答案中的整数解决方案非常优雅,但它无法为 double 类型返回 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)...)


S
Serge Rogatch

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

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


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

该函数假定:

浮点数的 binary32 表示

使用命名联合时对严格别名规则产生异常的编译器


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

J
Jagreet

当您可以简单地执行此操作时,为什么要使用三元运算符和 if-else

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

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