std::to_signed and std::to_unsigned

Document number:
P3643R5
Date:
2026-10-14
Audience:
LEWG
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
Reply-to:
Jan Schultke <janschultke@gmail.com>
GitHub Issue:
wg21.link/P3643/github
Source:
github.com/eisenwave/cpp-proposals/blob/main/src/to-signed-unsigned.cow

Add std::to_signed and std::to_unsigned function templates in the style of std::to_underlying.

Contents

1

Revision history

1.1

Changes since R4

1.2

Changes since R3

1.3

Changes since R2

1.4

Changes since R1

1.5

Changes since R0

2

Introduction

2.1

Existing developer practice

2.2

Rust cast_signed and cast_unsigned

2.3

Dual purpose

3

Impact on the standard

4

Design

4.1

Constraints

4.2

Handling of enumeration types

4.3

Naming

4.3.1

Possible criticism of naming

4.4

Saturating, checked, etc. variants (or lack thereof)

5

Possible implementation

6

Wording

7

Acknowledgements

8

References

1. Revision history

1.1. Changes since R4

1.2. Changes since R3

1.3. Changes since R2

1.4. Changes since R1

1.5. Changes since R0

2. Introduction

In integer numerics and bit-manipulation code, it is common to implement functionality in terms of the corresponding signed/unsigned type:

template<class T> T arithmetic_shift_right(T x, int s) { return T(std::make_signed_t<T>(x) >> s); } template<class T> T wrapping_add(T x, T y) { constexpr unsigned to_int_promotion_defense = 0; return T(to_int_promotion_defense + std::make_unsigned_t<T>(x) + std::make_unsigned_t<T>(y)); }

This technique technically works, but suffers from some problems:

The greater the distance between T and the use of std::make_{un}signed, the easier it is to make a mistake.

Another problem is that sometimes APIs are made usable only with unsigned integers, such as functions in the <bit> header (but this hurdle extends far beyond the standard library). Such APIs could often be used meaningfully with signed integers, which makes the restriction somewhat arbitrary. std::to_unsigned can make them usable, like in std::popcount(std::to_unsigned(123)).

To solve these issues, this proposal adds the function templates std::to_signed(x) and std::to_unsigned(x), which deduce T from x. This is concise and always uses the correct type.

2.1. Existing developer practice

A GitHub code search for

/[^a-zA-Z_](to_signed|to_unsigned)|static_cast<(typename)? ?((::)?std::)?(make_signed|make_unsigned)/
-is:fork language:c++

… shows that roughly 115K C++ files already use a non-standard to_unsigned and to_signed, or which static_cast to make_signed or make_unsigned.

By comparison,

/[^a-zA-Z_](to_underlying)|static_cast<(typename)? ?((::)?std::)?(underlying_type)/
-is:fork language:c++

… yields 165K C++ files that use to_underlying, or use static_cast with std::underlying_type.

The proposal [P1682R3] for std::to_underlying had similar rationale, and at the time, the author was only able to discover 1000 search results for to_underlying.

2.2. Rust cast_signed and cast_unsigned

As a point of reference, Rust also has cast_signed and cast_unsigned functions for integers. See also tracking issue for integer_sign_cast.

The problem statement in the libs-team issue applies equally to C++:

Casting using the as operator has some issues, in that it does not necessarily reflect the intent of the cast. Was the cast meant to be lossy or not? Was it meant to preserve the size? Constness? Signedness?

Similarly, a static_cast<std::int32_t>(x) from std::uint32_t does not reflect the purpose, while std::to_signed(x) expresses the intent of give me the same size and value, but reinterpreted as a signed type.

A GitHub code search for

/\bcast_(un)signed\b/ -is:fork/-is:fork language:rust

… shows roughly 5.1K Rust files.

2.3. Dual purpose

In addition to the purpose of changing signedness of integers, the proposed functions have another purpose: mapping other integral and enumeration types onto signed or unsigned integer types.

Assuming 32-bit unsigned, the use of to_unsigned reduces the number of template instantiations below:

// BAD, two instantiations of popcnt that are functionally equivalent: popcnt(U'x'); // calls popcnt<char32_t> popcnt(123u); // calls popcnt<unsigned> // GOOD, only one instantiation of popcnt: popcnt(to_unsigned(U'x')); // calls popcnt<unsigned> popcnt(to_unsigned(123u)); // calls popcnt<unsigned>

Of course, the instantiation of to_unsigned is added, but its cost is quickly outweighed by avoiding instantiations of much larger and complicated templates throughout the code base.

Just like make_signed and make_unsigned, to_signed and to_unsigned map the much larger set of integral types onto the smaller set of signed and unsigned integer types. This reduces template instantiations and may improve compilation speed and reduce code size.

3. Impact on the standard

This proposal is a pure library extension.

4. Design

This proposal follows precedent: similar to to_underlying, the proposed functions are located in <utility>. The naming scheme is based on to_underlying and the search results in §2.1. Existing developer practice.

4.1. Constraints

The design of to_signed and to_unsigned should be SFINAE-friendly because that is useful.

Being able to test whether std::to_signed(x) is valid lets us constrain the interface of arithmetic_shift_right.

template<class T> requires requires (T x) { std::to_signed(x); } T arithmetic_shift_right(T x, int s) { return T(std::to_signed(x) >> s); }

As for the constraints of the proposed functions, there are multiple options:

4.2. Handling of enumeration types

As mentioned above, anything accepted by std::make_signed is also accepted by std::to_signed, including enumeration types.

This allows usage such as:

enum E { A, B, C }; auto x = std::to_signed(A); auto y = std::bit_ceil(std::to_unsigned(B));

If enumerations weren't supported, an additional step through std::to_underlying would be required.

However, std::make_signed and std::make_unsigned shouldn't be used directly as the return type because they have long-standing design issues. std::make_unsigned surprisingly does not yield the underlying type of an enumeration type (or the unsigned counterpart thereof), but yields ([meta.trans.sign]):

the unsigned integer type with smallest rank ([conv.rank]) for which sizeof(T) == sizeof(type), with the same cv-qualifiers as T.

Consider an the following enumeration and assertion:

enum E : unsigned long long {}; static_assert(std::is_same_v<unsigned long, std::make_unsigned_t<E>>);

The assertion passes if sizeof(unsigned long) equals sizeof(unsigned long long).

It passes even if unsigned long cannot represent all values of unsigned long long. This can happen if unsigned long has the same size as unsigned long long but has more padding bits. However, this is mostly a theoretical case and may be resolvable via LWG issue.

A simple workaround is to use use std::make_unsigned_t<std::underlying_type_t<E>> for enumeration types.

4.3. Naming

As explained above, the proposed function is inspired by std::to_underlying, so the names std::to_signed and std::to_unsigned continue that existing naming scheme. That is, the word to generally implies a conversion in the standard library, possibly including value-changing conversions (as compared to as like in std::as_const, which indicates lossless conversions).

The name std::to_signed is consistent with the rest of the library in another way: all std::to_* functions are wrappers for a static_cast (where std::to_underlying and std::to_integer convert to the underlying type).

4.3.1. Possible criticism of naming

A possible counter-argument to the proposed names is that they don't make it readily apparent that wrapping takes place (e.g. std::to_unsigned(-1) yields UINT_MAX), as compared to a name like std::wrap_to_unsigned or std::wrapping_to_unsigned. However, the same could be said about all sorts of existing features that perform wrapping without expressing that in the name:

std::to_unsigned(-1) does the same as unsigned(-1). Neither notation advertises that wrapping takes place, but the behavior is consistent and intuitive. There is also only one plausible default interpretation of convert to unsigned in C++, which is what all of these operations are doing, although not every user is familiar enough with the language to arrive at that conclusion, so admittedly, some users won't intuit that std::to_unsigned wraps. Still, as long as the behavior doesn't deviate from the norm by being e.g. saturating or throwing, it shouldn't need explicit clarification.

Another counter-argument is that std::to_underlying and std::to_integer are value-preserving since they simply convert to the underlying type, so a user may have a false expectation of losslessness toward std::to_signed and std::to_unsigned.

Similarly, Rust provides signedness conversions via as u32 in the language and via cast_signed and cast_unsigned in the standard library (see §2.2. Rust cast_signed and cast_unsigned). C++ std::to_unsigned(-1) is essentially Rust (-1).cast_unsigned().

There is no explicit wrapping_cast_unsigned function despite Rust having wrapping operations like wrapping_add; cast_unsigned is considered sufficiently clear.

While C++ and Rust have some amount of naming alignment such as with std::saturating_add, adopting the Rust name verbatim would not make sense here because the cast_signed name is motivated by older names such as cast_mut (const_cast in C++). We also need to be consistent with pre-existing C++ names.

4.4. Saturating, checked, etc. variants (or lack thereof)

This proposal only adds the base operation, which is wrapping. It does not add any additional variants such as a checked, saturating, elastic, or other conversions. This has a number of reasons:

That being said, nothing precludes adding more variants in the future, just like we could always add a std::checked_gcd or std::saturating_bit_ceil. It's just not needed in this paper.

5. Possible implementation

template<class T> concept __sign_convertible = (is_integral_v<T> || is_enum_v<T>) && !is_same_v<T, bool> && !is_const_v<T> && !is_volatile_v<T>; template<class T> using __underlying_int_t = conditional_t<is_enum_v<T>, underlying_type_t<T>, T>; template<__sign_convertible T> constexpr make_signed_t<__underlying_int_t<T>> to_signed(T x) noexcept { return static_cast<make_signed_t<__underlying_int_t<T>>>(x); } template<__sign_convertible T> constexpr make_unsigned_t<__underlying_int_t<T>> to_unsigned(T x) noexcept { return static_cast<make_unsigned_t<__underlying_int_t<T>>>(x); }

6. Wording

The proposed wording is relative to [N5054].

In subclause [version.syn], add the following feature-testing macro:

#define __cpp_lib_to_signed 20XXXXL // freestanding, also in <utility>

In subclause [utility.syn], change the synopsis as follows:

namespace std { […] // [utility.sign.conv], sign conversion template<class T> using underlying-int-t = see below; // exposition only template<class T> constexpr make_signed_t<underlying-int-t<T>> to_signed(T value) noexcept; template<class T> constexpr make_unsigned_t<underlying-int-t<T>> to_unsigned(T value) noexcept; // [utility.underlying], to_underlying template<class T> constexpr underlying_type_t<T> to_underlying(T value) noexcept; […] }

In subclause [utility], add a subclause immediately prior to [utility.underlying]:

Sign conversion [utility.sign.conv]

The exposition-only alias template underlying-int-t is defined as follows:

template<class T> using underlying-int-t = conditional_t<is_enum_v<T>, underlying_type_t<T>, T>; template<class T> constexpr make_signed_t<underlying-int-t<T>> to_signed(T value) noexcept;

Constraints: T is a cv-unqualified integral or enumeration type other than bool.

Returns: static_cast<make_signed_t<underlying-int-t<T>>>(value) ([meta.trans.sign]).

template<class T> constexpr make_unsigned_t<underlying-int-t<T>> to_unsigned(T value) noexcept;

Constraints: T is a cv-unqualified integral or enumeration type other than bool.

Returns: static_cast<make_unsigned_t<underlying-int-t<T>>>(value) ([meta.trans.sign]).

Because make_unsigned_t uses Mandates, to_unsigned is arguably not SFINAE-friendly. For example, make_unsigned_t<int*> is ill-formed (?) without being instantiated. I don't attempt to fix this; I assume that the above specification "just works".

In practice, compilers do not eagerly instantiate make_signed_t when the constraints of the function template are not satisfied, so this specification works if implemented literally.

7. Acknowledgements

Thanks to Dalton Messmer for reviewing R1 of this paper, leaving thoughtful comments that contributed to an improved R2 which discusses many design aspects in more detail.

Thanks to Oliver Rosten and to BSI for providing extensive feedback on R3 of this paper, which resulted in improvements to the design discussion.

8. References

[N5054] Thomas Köppe. Working Draft, Programming Languages — C++ 2026-07-16 https://open-std.org/jtc1/sc22/wg21/docs/papers/2026/n5054.pdf
[P1682R3] JeanHeyd Meneide. std::to_underlying for enumerations 2021-01-22 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p1682r3.html