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};
是会报错的。
向下转换溢出的判断标准是真实的表数范围:
- 无符号数和对应的有符号数互转都算是溢出。
- 范围较小的无符号数转向范围较大的有符号数是不溢出的。
- 任何有符号数转向无符号数都会溢出,因为无符号数不能表示负数。
向下转换无法表示时的定义情况
向下转换无法表示时只有在 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
。
(Before LWG 2422)is_modulo
is false for signed integer types, unless the implementation defines signed integer overflow to wrap.
is_modulo
isfalse
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.