std::to_signed and std::to_unsigned

Document number:
P3643R3
Date:
2026-08-23
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 R2

1.2

Changes since R1

1.3

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

5

Possible implementation

6

Wording

7

Acknowledgements

8

References

1. Revision history

1.1. Changes since R2

1.2. Changes since R1

1.3. 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 are, the easier it is to make a mistake.

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 which use to_underlying, or which static_cast to 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 on the libs-team issue equally applies 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.

While C++ and Rust have some amount of naming alignment such as with std::saturating_add, that would not make sense here because the cast_signed name is motivated by older names such as cast_mut (const_cast in C++).

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 amount 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:

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<__sign_convertible T> constexpr make_signed_t<T> to_signed(T x) noexcept { return static_cast<make_signed_t<T>>(x); } template<__sign_convertible T> constexpr make_unsigned_t<T> to_unsigned(T x) noexcept { return static_cast<make_unsigned_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> constexpr make_signed_t<T> to_signed(T value) noexcept; template<class T> constexpr make_unsigned_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]

template<class T> constexpr make_signed_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<T>>(value) ([meta.trans.sign]).

template<class T> constexpr make_unsigned_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<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.

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