原始答案仅解决了将
unsigned
转换为
int
的问题。如果我们想要解决“某些无符号类型”到其相应的有符号类型的一般问题呢?此外,原始答案在引用标准部分和分析一些边角情况方面非常出色,但它并没有真正帮助我理解它为什么有效,因此这个答案将尝试给出一个强大的概念基础。这个答案将尝试解释“为什么”,并使用现代 C++ 特性来简化代码。
C++20 答案
随着 P0907:有符号整数是二进制补码 和被投票纳入 C++20 标准的 最终措辞 P1236,问题已经大大简化。现在,答案尽可能简单:
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)
)
)
)
);
}
}
这篇回答中有比被采纳的回答更多的强制类型转换,这是为了确保编译器不会发出有符号/无符号不匹配警告并正确处理整数提升规则。
首先,我们针对不是二进制补码的系统有一个特殊情况(因此我们必须特别处理最大可能值,因为它没有任何映射)。之后,我们进入真正的算法。
第二个顶层条件很简单:我们知道该值小于或等于最大值,因此它适合于结果类型。即使有注释,第三个条件也比较复杂,因此一些示例可能有助于理解为什么每个语句都是必要的。
概念基础:数轴
首先,什么是“窗口”概念?考虑以下数轴:
| signed |
<.........................>
| unsigned |
原来对于二进制补码整数,可以将可以通过任一类型到达的数字线子集分为三个大小相等的类别:
- => signed only
= => both
+ => unsigned only
<..-------=======+++++++..>
这可以通过考虑表示来轻松证明。无符号整数从
0
开始,并使用所有位以2的幂增加值。所有位对于有符号整数完全相同,除了符号位,其值为
-(2^position)
而不是
2^position
。这意味着对于所有
n-1
位,它们表示相同的值。然后,无符号整数有一个更多的正常位,将总值翻倍(换句话说,设置该位与未设置该位的值一样多)。有关有符号整数的逻辑相同,只是具有该位设置的所有值都为负。
另外两个合法的整数表示,反码和补码,与二进制补码整数具有相同的所有值,除了最小的负值。 C ++根据可表示值的范围定义整数类型的所有内容,除了
reinterpret_cast
(和C ++ 20
std :: bit_cast
)之外,而不是按位表示。这意味着只要我们不尝试创建陷阱表示,我们的分析就适用于这三种表示中的每一种。映射到此丢失值的无符号值是非常不幸的:刚好在无符号值的中间。幸运的是,我们的第一个条件检查(在编译时)是否存在这样的表示,并使用运行时检查特殊处理它。
第一个条件处理我们处于
=
部分的情况,这意味着我们处于重叠区域,在该区域中,可以在不更改值的情况下表示一种值为另一种值。代码中的
shift_by_window
函数将所有值向下移动每个这些段的大小(我们必须减去最大值然后减1以避免算术溢出问题)。如果我们在该区域之外(我们在
+
区域中),我们需要向下跳一个窗口大小。这将使我们进入重叠范围,这意味着我们可以安全地从无符号转换为有符号,因为值没有变化。但是,我们还没有完成,因为我们已经将两个无符号值映射到每个有符号值。因此,我们需要向下移动到下一个窗口(
-
区域),以便再次具有唯一映射。
现在,这是否给我们提供了与问题中请求的模UINT_MAX + 1
相一致的结果?UINT_MAX + 1
等同于2^n
,其中n
是值表示中位数的数量。我们用于窗口大小的值等于2^(n-1)
(序列中最后一个索引比大小少1)。我们将该值减去两次,这意味着我们将减去2 * 2^(n-1)
,它等于2^n
。在算术模x
中添加和减去x
是无操作的,因此我们没有影响原始值模2^n
。
正确处理整数提升
因为这是一个通用函数,而不仅仅是int
和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
在这里,我们可以看到clang、gcc和icc在-O2
和-O3
下不会为cast
和cast_to_signed_integer_basic
生成任何代码,而MSVC在/O2
下也不会生成任何代码,因此解决方案是最优的。
2 ^ 32-32768
个值压缩到32位中。并不是你的论点以任何方式依赖于“int”的大小。 - Steve Jessop