是否可以在编译时测试 C++ 中的类型是否支持负零?

Is it possible to test whether a type supports negative zero in C++ at compile time?

有没有一种方法可以编写类型特征来确定类型是否支持 C++ 中的负零(包括 sign-and-magnitude 等整数表示)?我没有看到任何直接这样做的东西,而且 std::signbit 似乎不是 constexpr

澄清一下:我问是因为我想知道这是否可能,无论用例是什么,如果有的话。

不幸的是,我想不出办法。事实上,C 标准认为类型表示不应该是程序员的关注点 (*),而只是告诉 实现者 他们应该做什么。

作为一名程序员,您需要知道的是:

  • 2-补码不是负整数的唯一可能表示
  • 可能存在负数 0
  • 整数算术运算不能return负0,只能按位运算

(*) 意见:了解内部表示可能会导致程序员使用盲目忽略严格别名规则的旧的良好优化。如果您将类型视为只能在标准操作中使用的不透明对象,那么可移植性问题就会更少...

最好的办法是在编译时排除有符号零的可能性,但永远不要完全肯定它在编译时的存在。 C++ 标准在编译时防止检查二进制表示有很长的路要走:

  • reinterpret_cast<char*>(&value)constexpr 中被禁止。
  • 使用union类型来规避constexpr中的上述规则也是被禁止的。
  • 对整数类型的零和负零的操作行为完全相同,按照 c++ 标准,无法区分。
  • 对于浮点运算,常量表达式中禁止除以零,因此测试 1/0.0 != 1/-0.0 是不可能的。

唯一可以测试的是整数类型的域是否足够密集以排除带符号的零:

template<typename T>
constexpr bool test_possible_signed_zero()
{
    using limits = std::numeric_limits<T>;
    if constexpr (std::is_fundamental_v<T> &&
           limits::is_exact &&
           limits::is_integer) {
        auto low = limits::min();
        auto high = limits::max();
        T carry = 1;
        // This is one of the simplest ways to check that
        // the max() - min() + 1 == 2 ** bits
        // without stepping out into undefined behavior.
        for (auto bits = limits::digits ; bits > 0 ; --bits) {
            auto adder = low % 2 + high %2 + carry;
            if (adder % 2 != 0) return true;
            carry = adder / 2;
            low /= 2;
            high /= 2;
        }
        return false;
    } else {
        return true;
    }
}

template <typename T>
class is_possible_signed_zero:
 public std::integral_constant<bool, test_possible_signed_zero<T>()>
{};
template <typename T>
constexpr bool is_possible_signed_zero_v = is_possible_signed_zero<T>::value;

仅保证如果此特征 returns false 则不可能有符号零。这个把握很弱,但我也看不出有什么更强的把握。此外,它对浮点类型没有任何建设性的说明。我找不到任何合理的方法来测试浮点类型。

C++ 中的标准 std::signbit 函数有一个接收整数值的构造函数

  • bool signbit( IntegralType arg ); (4) (C++11 起)

因此您可以查看 static_assert(signbit(-0))。但是有一个脚注(强调我的)

  1. A set of overloads or a function template accepting the arg argument of any integral type. Equivalent to (2) (the argument is cast to double).

不幸的是,这意味着您仍然必须依赖负零的浮点类型。您可以使用 std::numeric_limits<double>::is_iec559

强制使用带符号零的 IEEE-754

同样,std::copysign 具有可用于此目的的重载 Promoted copysign ( Arithmetic1 x, Arithmetic2 y );。不幸的是,根据当前标准,signbitcopysign 都不是 constexpr,尽管有一些建议

Clang and GCC can already consider those constexpr if you don't want to wait for the standard to update. Here's their results


具有负零的系统也有一个平衡范围,因此可以只检查正负范围是否具有相同的幅度

if constexpr(-std::numeric_limits<int>::max() != std::numeric_limits<int>::min() + 1) // or
if constexpr(-std::numeric_limits<int>::max() == std::numeric_limits<int>::min())
    // has negative zero

其实-INT_MAX - 1也是怎样libraries defined INT_MIN in two's complement

但最简单的解决方案是消除非补码情况,现在几乎不存在

static_assert(-1 == ~0, "This requires the use of 2's complement");

相关:

  • How to check a double's bit pattern is 0x0 in a C++11 constexpr?

有人会过来指出这是完全错误的标准。

不管怎么说,十进制机器不再被使用,古往今来只有一个负零。实际上,这些测试就足够了:

INT_MIN == -INT_MAX && ~0 == 0

但是您的代码不起作用有两个原因。不管标准怎么说,constexprs 是在主机上使用主机规则进行评估的,并且存在一个在编译时崩溃的架构。

试图按摩出陷阱是不可能的。 ~(unsigned)0 == (unsigned)-1 可靠地测试了 2s 的赞美,所以它的逆确实检查了一个人的赞美*;然而,~0 是对 ones compliment 生成负零的唯一方法,任何将该值用作有符号数的行为都可能陷入困境,因此我们无法测试其行为。即使使用特定于平台的代码,我们也无法在 constexpr 中捕获陷阱,所以算了吧。

*除非真正奇特的算术但是嘿

每个人都使用#define来进行架构选择。如果你需要知道,就用它。

如果您递给我一个实际符合标准的编译器,该编译器在 constexpr 中的陷阱上产生了编译错误,并使用目标平台规则而不是主机平台规则进行了评估并转换了结果,我们可以这样做:

target.o: target.c++
    $(CXX) -c target.c++ || $(CC) -DTRAP_ZERO -c target.c++

bool has_negativezero() {
#ifndef -DTRAP_ZERO
        return INT_MIN == -INT_MAX && ~0 == 0;
#else
        return 0;
#endif
}