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
and function templates
in the style of .
Contents
Revision history
Changes since R4
Changes since R3
Changes since R2
Changes since R1
Changes since R0
Introduction
Existing developer practice
Rust cast_signed and cast_unsigned
Dual purpose
Impact on the standard
Design
Constraints
Handling of enumeration types
Naming
Possible criticism of naming
Saturating, checked, etc. variants (or lack thereof)
Possible implementation
Wording
Acknowledgements
References
1. Revision history
1.1. Changes since R4
- added §4.2. Handling of enumeration types
- changed return type to avoid direct use of
andstd :: make_signed std :: make_unsigned
1.2. Changes since R3
- added §4.3. Naming
- added
as motivation in §2. Introductionstd :: popcount ( std :: to_unsigned ( 123 ) ) - added §4.4. Saturating, checked, etc. variants (or lack thereof)
1.3. Changes since R2
- updated GitHub code search result count in §2.1. Existing developer practice
- added §2.2. Rust
cast_signed andcast_unsigned - rebased §6. Wording on [N5054]
1.4. Changes since R1
- ported proposal from Bikeshed to COWEL
- added §2.3. Dual purpose
- reordered §4. Design and §5. Possible implementation
- added discussion on constraints in §4. Design
- fixed typo in §6. Wording:
xvalue
1.5. Changes since R0
-
tightened constraints in §6. Wording to exclude
cv-qualified types in general, not just cv
bool - added
comment to feature-test macrofreestanding
2. Introduction
In integer numerics and bit-manipulation code, it is common to implement functionality in terms of the corresponding signed/unsigned type:
This technique technically works, but suffers from some problems:
-
The use of C-style/function-style casts may conflict with the project's style.
When
is used instead, this code becomes substantially more verbose.static_cast -
Since
has to be spelled out in code, abbreviated function templates cannot be used instead (or at least,T would be required for the cast).decltype ( x ) -
Repeating the type
may be prone to mistakes. Nothing guarantees us thatT is of typeT when writing an expressionT . In larger code samples, mismatching types and variables is a bug waiting to happen. To be safe, we would have to writestd :: make_signed_t < T > ( x ) . However, this still repeats the expressionstd :: make_signed_t < decltype ( x ) > ( x ) .x
The greater the distance between and the use of
Another problem is that sometimes APIs are made usable only with unsigned integers,
such as functions in the 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.
can make them usable,
like in .
To solve these issues, this proposal adds the function templates
and ,
which deduce from .
This is concise and always uses the correct type.
2.1. Existing developer practice
A GitHub code search for
… shows that roughly 115K C++ files
already use a non-standard and ,
or which to or .
By comparison,
… yields 165K C++ files
that use ,
or use with .
The proposal [P1682R3] for had similar rationale, and at the time,
the author was only able to discover 1000 search results for .
2.2. Rust cast_signed and cast_unsigned
As a point of reference,
Rust also has and functions for integers.
See also tracking issue for
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 from
does not reflect the purpose,
while expresses the intent of
give me the same size and value, but reinterpreted as a signed type
.
A GitHub code search for
… 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.
,
the use of reduces the number of
template instantiations below:
Of course, the instantiation of is added,
but its cost is quickly outweighed by avoiding instantiations
of much larger and complicated templates throughout the code base.
Just like and ,
and 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 ,
the proposed functions are located in .
The naming scheme is based on
and the search results in §2.1. Existing developer practice.
4.1. Constraints
The design of and
should be SFINAE-friendly because that is useful.
is valid lets us constrain the interface of .
As for the constraints of the proposed functions, there are multiple options:
-
❌ Only permit signed or unsigned integer types.
This would be needlessly restrictive because, for example,
is also valid (and yields e.g.make_signed < char16_t > ), which makes it possible to implement thesigned short function in §2. Introduction forarithmetic_shift_right as well. In general, it would make generic programming harder if only some of the integral types were supported.char16_t -
✔️ Permit what
supports. That is, integral or enumeration types other thanmake_signed . This is proposed. There is no compelling reason to deviate from the restrictions imposed bybool andmake_signed , as their restrictions de facto define for which types a sign conversion makes sense.make_unsigned -
❌ Permit
types. This is out-of scope, and would effectively replaceinteger-like . Integer-like types are an open set of types, possibly including user-defined types. This effectively turns the proposed function into a customization point. Seeing thatto-unsigned-like is a concept which only exists in [ranges], the proposed functions should then beinteger-like andstd :: ranges :: to_signed . However, the goal of the proposal is to provide a simple numerical function, not add a ranges-specific customization point object.std :: ranges :: to_unsigned -
❌ Permit arithmetic types.
This would mirror
, which is alsostd :: is_signed_v for floating-point types. This would create a weird asymmetry, wheretrue is a no-op, andto_signed ( float ) is ill-formed. The result of the proposed functions is always a signed or unsigned integer type, and permittingto_unsigned ( float ) would muddy the waters here.float
4.2. Handling of enumeration types
As mentioned above,
anything accepted by is also accepted by ,
including enumeration types.
If enumerations weren't supported,
an additional step through would be required.
However, and
shouldn't be used directly as the return type
because they have long-standing design issues.
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
, with the same cv-qualifiers assizeof ( T ) == sizeof ( type ) .T
The assertion passes if
equals .
It passes even if cannot represent all values of .
This can happen if has the same size as
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
for enumeration types.
4.3. Naming
As explained above,
the proposed function is inspired by ,
so the names and
continue that existing naming scheme.
That is, the word
generally implies a conversion in the standard library,
possibly including value-changing conversions
(as compared to
like in ,
which indicates lossless conversions).
The name is consistent with the rest of the library in another way:
all functions are wrappers for a
(where and
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. yields ),
as compared to a name like
or .
However, the same could be said about all sorts of existing features
that perform wrapping without expressing that in the name:
- implicit conversions between
andint are wrappingunsigned - aliasing
objects viaint is wrappingreinterpret_cast < unsigned * > is wrappingstatic_cast < unsigned > ( - 1 ) is wrappingstd :: bit_cast < unsigned > ( - 1 ) - anything equivalent to a
(C-style cast, functional cast) is wrappingstatic_cast
does the same as .
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 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 and
are value-preserving since they simply convert to the underlying type,
so a user may have a false expectation of losslessness
toward and .
in the language and
via
is essentially
Rust .
There is no explicit function
despite Rust having wrapping operations like ;
is considered sufficiently clear.
,
adopting the Rust name verbatim
would not make sense here
because the name is motivated by older names such as
( 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:
-
Saturating and checked conversions don't make much design sense.
Remember that the proposed functions deliberately don't require the user
to specify the target width;
it is deduced from the input.
It makes very little sense to
combine this with checked or saturating forms
where an exception is thrown or the value is clamped when it falls outside a specific range.
We would be combining a
don't care about range
operation with acare a lot about range
form. -
Most of these forms don't exist in the C++ standard yet and should be explored holistically.
For example:
-
If we wanted
(whatever the check does), we should explorestd :: checked_to_signed andstd :: checked_add to make a coherent library, similar to saturation arithmetic.std :: checked_mul -
If we wanted a
operation (which when given an unsigned operand, returns some wider signed type so the conversion is lossless), we should explorestd :: elastic_to_signed andstd :: elastic_add .std :: elastic_mul It is unclear what a operation would do, given that unsigned types cannot represent negative values no matter how wide. This asymmetry and the overall lack of precedent in the standard library and core language makes elastic semantics particularly unsuitable for the basestd :: elastic_to_unsigned andstd :: to_signed operations.std :: to_unsigned
-
If we wanted
-
The one form that may be worth exploring is
, but as explained above, it makes little sense.std :: saturating_to_signed -
Deployment experience from Rust shows that there is virtually no user interest in such variants.
A GitHub code search for the nightly-only
functions, i.e. forinteger_cast_extras language:rust /(checked|saturating|strict)_cast_(un)?signed/ -is:fork yields only 30 results at the time of writing, of which almost all- are in comments,
- are part of the crate/standard library implementation (which is not user demand),
- act as pieces of a larger operation like a
normal
checked cast (making them arguably obsolete when such a combined operation exists), or - do two saturating conversions in a row (which is an anti-pattern
and obsolete when
exists).std :: saturating_cast
One could argue that these functions are recent and unstable, so it is too early to judge, but genuinely useful numeric functions like already acquired 1000K GitHub uses (from people independently implementing/copying pasting it), while libc++ and libstdc++ haven't released this feature yet.std :: bit_compress
That being said,
nothing precludes adding more variants in the future,
just like we could always add a
or .
It's just not needed in this paper.
5. Possible implementation
6. Wording
The proposed wording is relative to [N5054].
In subclause [version.syn], add the following feature-testing macro:
In subclause [utility.syn], change the synopsis as follows:
In subclause [utility], add a subclause immediately prior to [utility.underlying]:
Sign conversion [utility.sign.conv]
The exposition-only alias template
is defined as follows:
Constraints:
is a cv-unqualified integral or enumeration type other than .
Returns:
([meta.trans.sign]).
Constraints:
is a cv-unqualified integral or enumeration type other than .
Returns:
([meta.trans.sign]).
uses Mandates,
is arguably not SFINAE-friendly.
For example, 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
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.