C++20 之前向下转换到有符号数无法表示时是实现定义

什么是向下转换

向下转换是可能出现溢出的转换。检查是否为向下转换:

template <typename To, typename From, typename = void>
struct NotNarrowing: std::false_type {};

template <typename To, typename From>
struct NotNarrowing<To, From, std::void_t<decltype(To{std::declval<From>()})>>: std::true_type {};

其中 To{std::declval<From>()} 这一步如果用在具体的类型上(比如说 int 和 unsigned),编译器是只给警告,不会报错。如果能够在编译期推导出来数值,则会根据数值来选择通过或报错。比如 char{'a'}; 是正常编译的,但是 char{300}; 是会报错的。

向下转换溢出的判断标准是真实的表数范围:

  1. 无符号数和对应的有符号数互转都算是溢出。
  2. 范围较小的无符号数转向范围较大的有符号数是不溢出的。
  3. 任何有符号数转向无符号数都会溢出,因为无符号数不能表示负数。

向下转换无法表示时的定义情况

向下转换无法表示时只有在 unsigned 类型上有定义,在浮点数上无定义,在 signed 整数上是 implementation-defined(C++17)或模除定义(C++20,同 unsigned)。见 cppreference.com implicit conversion 页面中 Integral conversions 小节。

Caution

Operator arithmetic 页面 的 Overflows 小节说明:算术运算的溢出是未定义行为。要注意算术运算和向下类型转换是两个不同的场合。

因此,虽然可能会溢出,大范围的数字转向任意范围的无符号数都有良好定义。大范围的数字转向小范围的有符号数,在无法表示的时候仅在 C++20 以上有良好定义(否则是实现定义)。

{
  // implementation-defined in C++17
  extern long long f();
  signed a = f();
}
{
  // well-defined
  extern long long f();
  unsigned a = f();
}
{
  // won't compile
  extern long long f();
  unsigned a{ f() };
}

std::numeric_limits<T>::is_modulo 可以检查这种溢出是否是被定义为模除。但是对于有符号整数来说,不保证值都是 true(C++17),但如果具体实现将其定义为模除,这个值可以为 true

is_modulo is false for signed integer types, unless the implementation defines signed integer overflow to wrap. (Before LWG 2422)

is_modulo is false for signed integer types (6.8.2 basic.fundamental) unless an implementation, as an extension to this International Standard, defines signed integer overflow to wrap.