原始答案仅针对unsigned
=>解决了问题int
。如果我们想将“某些无符号类型”的一般问题解决为其对应的带符号类型,该怎么办?此外,原始答案在引用该标准的各个部分并分析某些极端情况时非常出色,但是它并不能真正帮助我理解它的工作原理,因此,该答案将为您提供坚实的概念基础。该答案将尝试帮助解释“为什么”,并使用现代C ++功能来简化代码。
C ++ 20答案
P0907极大地简化了该问题:带符号整数是Two的补码,最后的措词P1236被投票支持C ++ 20标准。现在,答案尽可能简单:
template<std::unsigned_integral T>
constexpr auto cast_to_signed_integer(T const value) {
return static_cast<std::make_signed_t<T>>(value);
}
而已。一个static_cast
(或C样式转换)终于保证你需要为这个问题的东西,东西很多程序员认为它总是这样。
C ++ 17答案
在C ++ 17中,事情要复杂得多。我们必须处理三种可能的整数表示形式(二的补码,一的补码和符号幅度)。即使在我们因为检查了可能值的范围而知道必须为二进制补码的情况下,将超出有符号整数范围的值转换为有符号整数仍会为我们提供实现定义的结果。我们必须使用在其他答案中看到的技巧。
首先,以下是用于一般解决问题的代码:
template<typename T, typename = std::enable_if_t<std::is_unsigned_v<T>>>
constexpr auto cast_to_signed_integer(T const value) {
using result = std::make_signed_t<T>;
using result_limits = std::numeric_limits<result>;
if constexpr (result_limits::min() + 1 != -result_limits::max()) {
if (value == static_cast<T>(result_limits::max()) + 1) {
throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
}
}
if (value <= result_limits::max()) {
return static_cast<result>(value);
} else {
using promoted_unsigned = std::conditional_t<sizeof(T) <= sizeof(unsigned), unsigned, T>;
using promoted_signed = std::make_signed_t<promoted_unsigned>;
constexpr auto shift_by_window = [](auto x) {
return x - static_cast<decltype(x)>(result_limits::max()) - 1;
};
return static_cast<result>(
shift_by_window(
static_cast<promoted_signed>(
shift_by_window(
static_cast<promoted_unsigned>(value)
)
)
)
);
}
}
它比可接受的答案多强制转换,这是为了确保编译器没有任何有符号/无符号不匹配警告,并正确处理整数提升规则。
对于非二进制补码的系统,我们首先有一个特殊情况(因此,我们必须特别处理最大可能值,因为它没有任何要映射的值)。之后,我们进入了真正的算法。
第二个顶层条件很简单:我们知道该值小于或等于最大值,因此适合结果类型。第三个条件即使带有注释也有些复杂,因此一些示例可能会有助于理解为什么每个语句都是必需的。
概念基础:数字线
首先,这个window
概念是什么?考虑以下数字行:
| signed |
<.........................>
| unsigned |
事实证明,对于二进制补码整数,您可以将任一类型可以到达的数字行的子集划分为三个大小相等的类别:
- => signed only
= => both
+ => unsigned only
<..-------=======+++++++..>
通过考虑表示可以很容易地证明这一点。一个无符号整数从处开始,0
并使用所有位以2的幂为单位增加值。除了符号位(值得-(2^position)
代替)之外,所有其他位的有符号整数完全相同2^position
。这意味着对于所有n - 1
位,它们表示相同的值。然后,无符号整数再增加一个普通位,这使值的总数增加了一倍(换句话说,设置该位的值与未设置该位的值一样多)。除了带该位设置的所有值均为负之外,带符号整数的逻辑相同。
其他两个合法的整数表示形式,一个的补码和符号幅度与所有两个补码整数具有相同的值,唯一的区别是:最大的负值。C ++根据可表示值的范围(而不是位表示)定义了有关整数类型的所有内容reinterpret_cast
((和C ++ 20std::bit_cast
除外))。这意味着只要我们从未尝试创建陷阱表示,我们的分析将适用于这三种表示中的每一个。映射到该缺失值的无符号值是一个非常不幸的值:在无符号值中间的一个右值。幸运的是,我们的第一个条件(在编译时)检查是否存在这样的表示形式,然后通过运行时检查专门对其进行处理。
第一个条件处理我们在该=
部分中的情况,这意味着我们在重叠区域中,一个区域中的值可以在另一个区域中表示,而无需更改。shift_by_window
代码中的函数将所有值向下移动每个段的大小(我们必须减去最大值然后减去1,以避免算术溢出问题)。如果我们不在该区域内(我们在该区+
域内),则需要向下跳转一个窗口大小。这使我们处于重叠范围内,这意味着我们可以安全地从无符号转换为有符号,因为值没有变化。但是,我们尚未完成,因为我们已将两个无符号值映射到每个有符号值。因此,我们需要向下移至下一个窗口(-
区域),以便我们再次拥有唯一的映射。
现在,这是否给我们提供了结果一致的mod UINT_MAX + 1
,如问题中所要求的?UINT_MAX + 1
等效于2^n
,其中n
是值表示形式中的位数。我们用于窗口大小的值等于2^(n - 1)
(值序列中的最后一个索引比大小小1)。我们将该值减去两次,这意味着我们减去2 * 2^(n - 1)
等于的值2^n
。加法和减法x
在算术mod中是不可操作的x
,因此我们没有影响原始值mod 2^n
。
正确处理整数促销
因为这是一个通用功能,而不仅仅是int
and unsigned
,所以我们还必须考虑完整的促销规则。有两种可能有趣的情况:一种short
小于int
,另一种short
与相同int
。
示例:short
小于int
如果short
小于int
(在现代平台上很常见),那么我们也知道unsigned short
可以适合int
,这意味着对它的任何操作实际上都将发生在中int
,因此我们明确地转换为提升类型以避免这种情况。我们的最终声明非常抽象,如果我们替换为真实值,则变得更容易理解。对于第一个有趣的情况,在不失一般性的情况下,让我们考虑一个16位short
和一个17位int
(在新规则下仍然允许,并且仅表示这两个整数类型中的至少一个具有一些填充位) ):
constexpr auto shift_by_window = [](auto x) {
return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
shift_by_window(
static_cast<int17_t>(
shift_by_window(
static_cast<uint17_t>(value)
)
)
)
);
解决最大的16位无符号值
constexpr auto shift_by_window = [](auto x) {
return x - static_cast<decltype(x)>(32767) - 1;
};
return int16_t(
shift_by_window(
int17_t(
shift_by_window(
uint17_t(65535)
)
)
)
);
简化为
return int16_t(
int17_t(
uint17_t(65535) - uint17_t(32767) - 1
) -
int17_t(32767) -
1
);
简化为
return int16_t(
int17_t(uint17_t(32767)) -
int17_t(32767) -
1
);
简化为
return int16_t(
int17_t(32767) -
int17_t(32767) -
1
);
简化为
return int16_t(-1);
我们投入最大可能的未签名,并获得-1
成功!
示例:short
与int
如果short
大小相同int
(在现代平台上不常见),则积分促销规则会略有不同。在这种情况下,short
提升到int
和unsigned short
促进对unsigned
。幸运的是,我们将每个结果显式转换为我们要进行计算的类型,因此最终不会出现问题升级。不失一般性,让我们考虑一个16位short
和一个16位int
:
constexpr auto shift_by_window = [](auto x) {
return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
shift_by_window(
static_cast<int16_t>(
shift_by_window(
static_cast<uint16_t>(value)
)
)
)
);
解决最大的16位无符号值
auto x = int16_t(
uint16_t(65535) - uint16_t(32767) - 1
);
return int16_t(
x - int16_t(32767) - 1
);
简化为
return int16_t(
int16_t(32767) - int16_t(32767) - 1
);
简化为
return int16_t(-1);
我们投入最大可能的未签名,并获得-1
成功!
如果我只关心什么int
和unsigned
不关心的警告,就像原来的问题?
constexpr int cast_to_signed_integer(unsigned const value) {
using result_limits = std::numeric_limits<int>;
if constexpr (result_limits::min() + 1 != -result_limits::max()) {
if (value == static_cast<unsigned>(result_limits::max()) + 1) {
throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
}
}
if (value <= result_limits::max()) {
return static_cast<int>(value);
} else {
constexpr int window = result_limits::min();
return static_cast<int>(value + window) + window;
}
}
现场观看
https://godbolt.org/z/74hY81
在这里,我们看到,铛,GCC和ICC不产生代码cast
,并cast_to_signed_integer_basic
在-O2
和-O3
,和MSVC产生的任何代码/O2
,因此该解决方案是最优的。