避免实现定义的行为的有效无符号到符号转换

我想定义一个函数,它接受一个 unsigned int作为参数,并将一个 int全等模 UINT _ MAX + 1返回给参数。

第一次尝试可能是这样的:

int unsigned_to_signed(unsigned n)
{
return static_cast<int>(n);
}

但是任何语言律师都知道,对于大于 INT _ MAX 的值,从无符号到有符号的转换是实现定义的。

我想要实现这样的功能: (a)它只依赖于规范要求的行为; (b)它在任何现代机器和编译器最佳化上都编译成禁止操作。

至于奇异的机器... 如果对于无符号整型没有符号整型同余模 UINT _ MAX + 1,假设我想抛出一个异常。如果有不止一个(我不确定这是否可能) ,让我们假设我想要最大的一个。

好吧,第二次尝试:

int unsigned_to_signed(unsigned n)
{
int int_n = static_cast<int>(n);


if (n == static_cast<unsigned>(int_n))
return int_n;


// else do something long and complicated
}

当我不使用典型的二补体系统时,我不太关心效率,因为在我看来这是不可能的。如果我的代码成为2050年无处不在的星等系统的瓶颈,那么,我打赌有人可以找到并优化它。

现在,第二次尝试非常接近我想要的。尽管对于某些输入,对 int的转换是实现定义的,但是标准保证了对 unsigned的转换能够保留模 UINT _ MAX + 1的值。所以这个条件确实检查了我想要的东西,并且在我可能遇到的任何系统上它都不会被编译成任何东西。

但是... ... 我仍然在向 int转换,而没有首先检查它是否会调用实现定义的行为。在2050年的某个假设系统上,它可能会做出不知道什么事情。假设我想避免这种情况。

问题: 我的“第三次尝试”应该是什么样子的?

总结一下,我想说的是:

  • 从无符号整型转换为有符号整型
  • 保留值 mod UINT _ MAX + 1
  • 仅调用标准强制行为
  • 编译成一个典型的带有编译器最佳化的两人互补机器上的禁止操作

[更新]

让我举个例子来说明为什么这不是一个无关紧要的问题。

考虑一个假设的 C + + 实现,它具有以下属性:

  • sizeof(int)等于4
  • sizeof(unsigned)等于4
  • INT_MAX等于32767
  • INT_MIN等于 -2 32 + 32768
  • UINT_MAX等于232-1
  • int上的算法是模232(在 INT_MININT_MAX的范围内)
  • std::numeric_limits<int>::is_modulo是真的
  • 将无符号 n转换为 int 将保留0 < = n < = 32767的值,否则将生成

在这个假设的实现中,每个 unsigned值恰好有一个 int值同余(mod UINT _ MAX + 1)。所以我的问题很明确。

我声称这个假设的 C + + 实现完全符合 C + + 98、 C + + 03和 C + + 11规范。我承认我没有记住所有的单词... ... 但是我相信我已经仔细阅读了相关的章节。所以如果你想让我接受你的答案,你要么(a)引用一个规范来排除这个假设的实现,要么(b)正确处理它。

事实上,正确的答案必须处理标准所允许的 每个假设实现。根据定义,这就是“仅调用标准强制行为”的含义。

顺便说一句,请注意,由于多种原因,std::numeric_limits<int>::is_modulo在这里完全没有用处。首先,即使无符号到有符号强制转换对于大的无符号值不起作用,它也可以是 true。另一方面,它可以是 true,甚至在一个人的补充或符号幅度系统,如果算术是简单的模整数范围。诸如此类。如果你的答案取决于 is_modulo,那就错了。

[更新2]

Hvd 的回答 教会了我一些东西: 我假设的用于整数的 C + + 实现是现代 C 所允许的 没有。C99和 C11标准对于有符号整数的表示非常具体; 实际上,它们只允许二补、一补和符号大小(6.2.6.2节(2) ;)。

但是 C + + 不是 C。事实证明,这个事实正是我问题的核心。

最初的 C + + 98标准是基于更老的 C89,它说(3.1.2.5节) :

对于每个有符号整数类型,都有一个对应的(但是 不同)无符号整数类型(用关键字指定) 使用相同数量的存储(包括符号) 信息) ,并具有相同的对齐要求 有符号整数类型的非负值是 对应的无符号整数类型,以及 每种类型中的相同值是相同的。

C89没有提到只有一个符号位或只允许两个补码/一个补码/符号量级。

C + + 98标准几乎一字不差地采用了这种语言(3.9.1节第(3)段) :

对于每个有符号整数类型,都存在一个对应的 (但不同) 无符号整数类型: “ unsigned char”,“ < code > unsigned 短 int ”、“ unsigned int”和“ unsigned long int”,分别是 它占用相同的存储量并具有相同的对齐方式 要求(3.9)作为相应的有符号整数类型; 是,每个 有符号整数类型具有与 其相应的 无符号整数无符号整数类型。非负的范围 有符号整数类型的值是相应的 无符号整数类型,以及每个 相应的有符号/无符号类型必须相同。

C + + 03标准和 C + + 11使用基本相同的语言。

据我所知,没有任何标准 C + + 规范将其有符号整数表示约束到任何 C 规范中。而且没有强制要求一个单一的符号位或任何类似的东西。它所说的只是 非阴性有符号整数必须是对应的无符号整数的一个子范围。

因此,我再次声明 INT _ MAX = 32767和 INT _ MIN =-232 + 32768是允许的。如果你的回答假设不是这样,那么它是不正确的,除非你引用 C + + 标准来证明我是错误的。

58603 次浏览

If x is our input...

If x > INT_MAX, we want to find a constant k such that 0 < x - k*INT_MAX < INT_MAX.

This is easy -- unsigned int k = x / INT_MAX;. Then, let unsigned int x2 = x - k*INT_MAX;

We can now cast x2 to int safely. Let int x3 = static_cast<int>(x2);

We now want to subtract something like UINT_MAX - k * INT_MAX + 1 from x3, if k > 0.

Now, on a 2s complement system, so long as x > INT_MAX, this works out to:

unsigned int k = x / INT_MAX;
x -= k*INT_MAX;
int r = int(x);
r += k*INT_MAX;
r -= UINT_MAX+1;

Note that UINT_MAX+1 is zero in C++ guaranteed, the conversion to int was a noop, and we subtracted k*INT_MAX then added it back on "the same value". So an acceptable optimizer should be able to erase all that tomfoolery!

That leaves the problem of x > INT_MAX or not. Well, we create 2 branches, one with x > INT_MAX, and one without. The one without does a strait cast, which the compiler optimizes to a noop. The one with ... does a noop after the optimizer is done. The smart optimizer realizes both branches to the same thing, and drops the branch.

Issues: if UINT_MAX is really large relative to INT_MAX, the above might not work. I am assuming that k*INT_MAX <= UINT_MAX+1 implicitly.

We could probably attack this with some enums like:

enum { divisor = UINT_MAX/INT_MAX, remainder = UINT_MAX-divisor*INT_MAX };

which work out to 2 and 1 on a 2s complement system I believe (are we guaranteed for that math to work? That's tricky...), and do logic based on these that easily optimize away on non-2s complement systems...

This also opens up the exception case. It is only possible if UINT_MAX is much larger than (INT_MIN-INT_MAX), so you can put your exception code in an if block asking exactly that question somehow, and it won't slow you down on a traditional system.

I'm not exactly sure how to construct those compile-time constants to deal correctly with that.

std::numeric_limits<int>::is_modulo is a compile time constant. so you can use it for template specialization. problem solved, at least if compiler plays along with inlining.

#include <limits>
#include <stdexcept>
#include <string>


#ifdef TESTING_SF
bool const testing_sf = true;
#else
bool const testing_sf = false;
#endif


// C++ "extensions"
namespace cppx {
using std::runtime_error;
using std::string;


inline bool hopefully( bool const c ) { return c; }
inline bool throw_x( string const& s ) { throw runtime_error( s ); }


}  // namespace cppx


// C++ "portability perversions"
namespace cppp {
using cppx::hopefully;
using cppx::throw_x;
using std::numeric_limits;


namespace detail {
template< bool isTwosComplement >
int signed_from( unsigned const n )
{
if( n <= unsigned( numeric_limits<int>::max() ) )
{
return static_cast<int>( n );
}


unsigned const u_max = unsigned( -1 );
unsigned const u_half = u_max/2 + 1;


if( n == u_half )
{
throw_x( "signed_from: unsupported value (negative max)" );
}


int const i_quarter = static_cast<int>( u_half/2 );
int const int_n1 = static_cast<int>( n - u_half );
int const int_n2 = int_n1 - i_quarter;
int const int_n3 = int_n2 - i_quarter;


hopefully( n == static_cast<unsigned>( int_n3 ) )
|| throw_x( "signed_from: range error" );


return int_n3;
}


template<>
inline int signed_from<true>( unsigned const n )
{
return static_cast<int>( n );
}
}    // namespace detail


inline int signed_from( unsigned const n )
{
bool const is_modulo = numeric_limits< int >::is_modulo;
return detail::signed_from< is_modulo && !testing_sf >( n );
}
}    // namespace cppp


#include <iostream>
using namespace std;
int main()
{
int const x = cppp::signed_from( -42u );
wcout << x << endl;
}


EDIT: Fixed up code to avoid possible trap on non-modular-int machines (only one is known to exist, namely the archaically configured versions of the Unisys Clearpath). For simplicity this is done by not supporting the value -2n-1 where n is the number of int value bits, on such machine (i.e., on the Clearpath). in practice this value will not be supported by the machine either (i.e., with sign-and-magnitude or 1’s complement representation).

You can explicitly tell the compiler what you want to do:

int unsigned_to_signed(unsigned n) {
if (n > INT_MAX) {
if (n <= UINT_MAX + INT_MIN) {
throw "no result";
}
return static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1);
} else {
return static_cast<int>(n);
}
}

Compiles with gcc 4.7.2 for x86_64-linux (g++ -O -S test.cpp) to

_Z18unsigned_to_signedj:
movl    %edi, %eax
ret

Expanding on user71404's answer:

int f(unsigned x)
{
if (x <= INT_MAX)
return static_cast<int>(x);


if (x >= INT_MIN)
return static_cast<int>(x - INT_MIN) + INT_MIN;


throw x; // Or whatever else you like
}

If x >= INT_MIN (keep the promotion rules in mind, INT_MIN gets converted to unsigned), then x - INT_MIN <= INT_MAX, so this won't have any overflow.

If that is not obvious, take a look at the claim "If x >= -4u, then x + 4 <= 3.", and keep in mind that INT_MAX will be equal to at least the mathematical value of -INT_MIN - 1.

On the most common systems, where !(x <= INT_MAX) implies x >= INT_MIN, the optimizer should be able (and on my system, is able) to remove the second check, determine that the two return statements can be compiled to the same code, and remove the first check too. Generated assembly listing:

__Z1fj:
LFB6:
.cfi_startproc
movl    4(%esp), %eax
ret
.cfi_endproc

The hypothetical implementation in your question:

  • INT_MAX equals 32767
  • INT_MIN equals -232 + 32768

is not possible, so does not need special consideration. INT_MIN will be equal to either -INT_MAX, or to -INT_MAX - 1. This follows from C's representation of integer types (6.2.6.2), which requires n bits to be value bits, one bit to be a sign bit, and only allows one single trap representation (not including representations that are invalid because of padding bits), namely the one that would otherwise represent negative zero / -INT_MAX - 1. C++ doesn't allow any integer representations beyond what C allows.

Update: Microsoft's compiler apparently does not notice that x > 10 and x >= 11 test the same thing. It only generates the desired code if x >= INT_MIN is replaced with x > INT_MIN - 1u, which it can detect as the negation of x <= INT_MAX (on this platform).

[Update from questioner (Nemo), elaborating on our discussion below]

I now believe this answer works in all cases, but for complicated reasons. I am likely to award the bounty to this solution, but I want to capture all the gory details in case anybody cares.

Let's start with C++11, section 18.3.3:

Table 31 describes the header <climits>.

...

The contents are the same as the Standard C library header <limits.h>.

Here, "Standard C" means C99, whose specification severely constrains the representation of signed integers. They are just like unsigned integers, but with one bit dedicated to "sign" and zero or more bits dedicated to "padding". The padding bits do not contribute to the value of the integer, and the sign bit contributes only as twos-complement, ones-complement, or sign-magnitude.

Since C++11 inherits the <climits> macros from C99, INT_MIN is either -INT_MAX or -INT_MAX-1, and hvd's code is guaranteed to work. (Note that, due to the padding, INT_MAX could be much less than UINT_MAX/2... But thanks to the way signed->unsigned casts work, this answer handles that fine.)

C++03/C++98 is trickier. It uses the same wording to inherit <climits> from "Standard C", but now "Standard C" means C89/C90.

All of these -- C++98, C++03, C89/C90 -- have the wording I give in my question, but also include this (C++03 section 3.9.1 paragraph 7):

The representations of integral types shall define values by use of a pure binary numeration system.(44) [Example: this International Standard permits 2’s complement, 1’s complement and signed magnitude representations for integral types.]

Footnote (44) defines "pure binary numeration system":

A positional representation for integers that uses the binary digits 0 and 1, in which the values represented by successive bits are additive, begin with 1, and are multiplied by successive integral power of 2, except perhaps for the bit with the highest position.

What is interesting about this wording is that it contradicts itself, because the definition of "pure binary numeration system" does not permit a sign/magnitude representation! It does allow the high bit to have, say, the value -2n-1 (twos complement) or -(2n-1-1) (ones complement). But there is no value for the high bit that results in sign/magnitude.

Anyway, my "hypothetical implementation" does not qualify as "pure binary" under this definition, so it is ruled out.

However, the fact that the high bit is special means we can imagine it contributing any value at all: A small positive value, huge positive value, small negative value, or huge negative value. (If the sign bit can contribute -(2n-1-1), why not -(2n-1-2)? etc.)

So, let's imagine a signed integer representation that assigns a wacky value to the "sign" bit.

A small positive value for the sign bit would result in a positive range for int (possibly as large as unsigned), and hvd's code handles that just fine.

A huge positive value for the sign bit would result in int having a maximum larger than unsigned, which is is forbidden.

A huge negative value for the sign bit would result in int representing a non-contiguous range of values, and other wording in the spec rules that out.

Finally, how about a sign bit that contributes a small negative quantity? Could we have a 1 in the "sign bit" contribute, say, -37 to the value of the int? So then INT_MAX would be (say) 231-1 and INT_MIN would be -37?

This would result in some numbers having two representations... But ones-complement gives two representations to zero, and that is allowed according to the "Example". Nowhere does the spec say that zero is the only integer that might have two representations. So I think this new hypothetical is allowed by the spec.

Indeed, any negative value from -1 down to -INT_MAX-1 appears to be permissible as a value for the "sign bit", but nothing smaller (lest the range be non-contiguous). In other words, INT_MIN might be anything from -INT_MAX-1 to -1.

Now, guess what? For the second cast in hvd's code to avoid implementation-defined behavior, we just need x - (unsigned)INT_MIN less than or equal to INT_MAX. We just showed INT_MIN is at least -INT_MAX-1. Obviously, x is at most UINT_MAX. Casting a negative number to unsigned is the same as adding UINT_MAX+1. Put it all together:

x - (unsigned)INT_MIN <= INT_MAX

if and only if

UINT_MAX - (INT_MIN + UINT_MAX + 1) <= INT_MAX
-INT_MIN-1 <= INT_MAX
-INT_MIN <= INT_MAX+1
INT_MIN >= -INT_MAX-1

That last is what we just showed, so even in this perverse case, the code actually works.

That exhausts all of the possibilities, thus ending this extremely academic exercise.

Bottom line: There is some seriously under-specified behavior for signed integers in C89/C90 that got inherited by C++98/C++03. It is fixed in C99, and C++11 indirectly inherits the fix by incorporating <limits.h> from C99. But even C++11 retains the self-contradictory "pure binary representation" wording...

This code relies only on behavior, mandated by the spec, so requirement (a) is easily satisfied:

int unsigned_to_signed(unsigned n)
{
int result = INT_MAX;


if (n > INT_MAX && n < INT_MIN)
throw runtime_error("no signed int for this number");


for (unsigned i = INT_MAX; i != n; --i)
--result;


return result;
}

It's not so easy with requirement (b). This compiles into a no-op with gcc 4.6.3 (-Os, -O2, -O3) and with clang 3.0 (-Os, -O, -O2, -O3). Intel 12.1.0 refuses to optimize this. And I have no info about Visual C.

I think the int type is at least two bytes, so the INT_MIN and INT_MAX may change in different platforms.

Fundamental types

≤climits≥ header

My money is on using memcpy. Any decent compiler knows to optimise it away:

#include <stdio.h>
#include <memory.h>
#include <limits.h>


static inline int unsigned_to_signed(unsigned n)
{
int result;
memcpy( &result, &n, sizeof(result));
return result;
}


int main(int argc, const char * argv[])
{
unsigned int x = UINT_MAX - 1;
int xx = unsigned_to_signed(x);
return xx;
}

For me (Xcode 8.3.2, Apple LLVM 8.1, -O3), that produces:

_main:                                  ## @main
Lfunc_begin0:
.loc    1 21 0                  ## /Users/Someone/main.c:21:0
.cfi_startproc
## BB#0:
pushq    %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq    %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
##DEBUG_VALUE: main:argc <- %EDI
##DEBUG_VALUE: main:argv <- %RSI
Ltmp3:
##DEBUG_VALUE: main:x <- 2147483646
##DEBUG_VALUE: main:xx <- 2147483646
.loc    1 24 5 prologue_end     ## /Users/Someone/main.c:24:5
movl    $-2, %eax
popq    %rbp
retq
Ltmp4:
Lfunc_end0:
.cfi_endproc

The original answer solved the problem only for unsigned => int. What if we want to solve the general problem of "some unsigned type" to its corresponding signed type? Furthermore, the original answer was excellent at citing sections of the standard and analyzing some corner cases, but it did not really help me get a feel for why it worked, so this answer will try to give a strong conceptual basis. This answer will try to help explain "why", and use modern C++ features to try to simplify the code.

C++20 answer

The problem has simplified dramatically with P0907: Signed Integers are Two’s Complement and the final wording P1236 that was voted into the C++20 standard. Now, the answer is as simple as possible:

template<std::unsigned_integral T>
constexpr auto cast_to_signed_integer(T const value) {
return static_cast<std::make_signed_t<T>>(value);
}

That's it. A static_cast (or C-style cast) is finally guaranteed to do the thing you need for this question, and the thing many programmers thought it always did.

C++17 answer

In C++17, things are much more complicated. We have to deal with three possible integer representations (two's complement, ones' complement, and sign-magnitude). Even in the case where we know it must be two's complement because we checked the range of possible values, the conversion of a value outside the range of the signed integer to that signed integer still gives us an implementation-defined result. We have to use tricks like we have seen in other answers.

First, here is the code for how to solve the problem generically:

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) {
// static_cast to avoid conversion warning
return x - static_cast<decltype(x)>(result_limits::max()) - 1;
};
return static_cast<result>(
shift_by_window( // shift values from common range to negative range
static_cast<promoted_signed>(
shift_by_window( // shift large values into common range
static_cast<promoted_unsigned>(value) // cast to avoid promotion to int
)
)
)
);
}
}


This has a few more casts than the accepted answer, and that is to ensure there are no signed / unsigned mismatch warnings from your compiler and to properly handle integer promotion rules.

We first have a special case for systems that are not two's complement (and thus we must handle the maximum possible value specially because it doesn't have anything to map to). After that, we get to the real algorithm.

The second top-level condition is straightforward: we know the value is less than or equal to the maximum value, so it fits in the result type. The third condition is a little more complicated even with the comments, so some examples would probably help understand why each statement is necessary.

Conceptual basis: the number line

First, what is this window concept? Consider the following number line:

   |   signed   |
<.........................>
|  unsigned  |

It turns out that for two's complement integers, you can divide the subset of the number line that can be reached by either type into three equally sized categories:

- => signed only
= => both
+ => unsigned only


<..-------=======+++++++..>


This can be easily proven by considering the representation. An unsigned integer starts at 0 and uses all of the bits to increase the value in powers of 2. A signed integer is exactly the same for all of the bits except the sign bit, which is worth -(2^position) instead of 2^position. This means that for all n - 1 bits, they represent the same values. Then, unsigned integers have one more normal bit, which doubles the total number of values (in other words, there are just as many values with that bit set as without it set). The same logic holds for signed integers, except that all the values with that bit set are negative.

The other two legal integer representations, ones' complement and sign-magnitude, have all of the same values as two's complement integers except for one: the most negative value. C++ defines everything about integer types, except for reinterpret_cast (and the C++20 std::bit_cast), in terms of the range of representable values, not in terms of the bit representation. This means that our analysis will hold for each of these three representations as long as we do not ever try to create the trap representation. The unsigned value that would map to this missing value is a rather unfortunate one: the one right in the middle of the unsigned values. Fortunately, our first condition checks (at compile time) whether such a representation exists, and then handles it specially with a runtime check.

The first condition handles the case where we are in the = section, which means that we are in the overlapping region where the values in one can be represented in the other without change. The shift_by_window function in the code moves all values down by the size of each of these segments (we have to subtract the max value then subtract 1 to avoid arithmetic overflow issues). If we are outside of that region (we are in the + region), we need to jump down by one window size. This puts us in the overlapping range, which means we can safely convert from unsigned to signed because there is no change in value. However, we are not done yet because we have mapped two unsigned values to each signed value. Therefore, we need to shift down to the next window (the - region) so that we have a unique mapping again.

Now, does this give us a result congruent mod UINT_MAX + 1, as requested in the question? UINT_MAX + 1 is equivalent to 2^n, where n is the number of bits in the value representation. The value we use for our window size is equal to 2^(n - 1) (the final index in a sequence of values is one less than the size). We subtract that value twice, which means we subtract 2 * 2^(n - 1) which is equal to 2^n. Adding and subtracting x is a no-op in arithmetic mod x, so we have not affected the original value mod 2^n.

Properly handling integer promotions

Because this is a generic function and not just int and unsigned, we also have to concern ourselves with integral promotion rules. There are two possibly interesting cases: one in which short is smaller than int and one in which short is the same size as int.

Example: short smaller than int

If short is smaller than int (common on modern platforms) then we also know that unsigned short can fit in an int, which means that any operations on it will actually happen in int, so we explicitly cast to the promoted type to avoid this. Our final statement is pretty abstract and becomes easier to understand if we substitute in real values. For our first interesting case, with no loss of generality let us consider a 16-bit short and a 17-bit int (which is still allowed under the new rules, and would just mean that at least one of those two integer types have some padding bits):

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)
)
)
)
);

Solving for the greatest possible 16-bit unsigned value

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)
)
)
)
);

Simplifies to

return int16_t(
int17_t(
uint17_t(65535) - uint17_t(32767) - 1
) -
int17_t(32767) -
1
);

Simplifies to

return int16_t(
int17_t(uint17_t(32767)) -
int17_t(32767) -
1
);

Simplifies to

return int16_t(
int17_t(32767) -
int17_t(32767) -
1
);

Simplifies to

return int16_t(-1);

We put in the largest possible unsigned and get back -1, success!

Example: short same size as int

If short is the same size as int (uncommon on modern platforms), the integral promotion rule are slightly different. In this case, short promotes to int and unsigned short promotes to unsigned. Fortunately, we explicitly cast each result to the type we want to do the calculation in, so we end up with no problematic promotions. With no loss of generality let us consider a 16-bit short and a 16-bit 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)
)
)
)
);

Solving for the greatest possible 16-bit unsigned value

auto x = int16_t(
uint16_t(65535) - uint16_t(32767) - 1
);
return int16_t(
x - int16_t(32767) - 1
);

Simplifies to

return int16_t(
int16_t(32767) - int16_t(32767) - 1
);

Simplifies to

return int16_t(-1);

We put in the largest possible unsigned and get back -1, success!

What if I just care about int and unsigned and don't care about warnings, like the original question?

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;
}
}

See it live

https://godbolt.org/z/74hY81

Here we see that clang, gcc, and icc generate no code for cast and cast_to_signed_integer_basic at -O2 and -O3, and MSVC generates no code at /O2, so the solution is optimal.