P3643R0
std::to_signed and std::to_unsigned

Published Proposal,

This version:
https://eisenwave.github.io/cpp-proposals/to-signed-unsigned.html
Author:
Audience:
LEWG
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
Source:
eisenwave/cpp-proposals

Abstract

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

1. Introduction

In integer numerics and bit-manipulation code, it is common to implement functionality in terms of the corresponding signed/unsigned type. The most concise form is a function-style cast with a very short type name.

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

However, this is problematic for two reasons:

  1. The use of C-style/function-style casts may conflict with the project’s style. When static_cast is used instead, this code becomes substantially more verbose.

  2. Repeating the type T violates the DRY (Don’t Repeat Yourself) principle in software design. Nothing guarantees us that x is of type T when writing an expression std::make_signed_t<T>(x). In larger code samples, mismatching types and variables is a bug waiting to happen. To be safe, we would have to write std::make_signed_t<decltype(x)>(x). However, now we are repeating the expression x, so we haven’t fully solved the problem.

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.

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 12.9K C++ files already use a non-standard to_unsigned and to_signed, or 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 30.8K C++ files which use to_underlying or static_cast to std::underlying_type, of which 11.6K C++ files convert via static_cast.

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. Impact on the standard

This proposal is a pure library extension.

Note: [ranges.syn] already defines an exposition-only function to-unsigned-like, however, this is more powerful than the proposed function because it operates on unsigned-like types, not unsigned integer types. Therefore, the wording in [ranges] remains unaffected.

3. Possible implementation

template<class T>
constexpr std::make_signed_t<T> to_signed(T x) noexcept {
    return static_cast<std::make_signed_t<T>>(x);
}

template<class T>
constexpr std::make_unsigned_t<T> to_unsigned(T x) noexcept {
    return static_cast<std::make_unsigned_t<T>>(x);
}

4. Design decisions

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 result in § 1 Introduction.

5. Proposed wording

The proposed wording is relative to [N5001].

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

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

In subclause [utility.syn], update 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 an integral or enumeration type other than cv bool.

Returns: static_cast<make_signed_t<T>>(x).

template<class T>
  constexpr make_unsigned_t<T> to_unsigned(T value) noexcept;

Constraints: T is an integral or enumeration type other than cv bool.

Returns: static_cast<make_unsigned_t<T>>(x).

Note: Because make_unsigned_t Mandates that T is an integral type, to_unsigned is arguably not SFINAE-friendly because say, 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".

Note: The name of the subclause is based on [meta.trans.sign], sign modifications.

References

Normative References

[N5001]
Thomas Köppe. Working Draft, Programming Languages — C++. 17 December 2024. URL: https://wg21.link/n5001

Informative References

[P1682R3]
JeanHeyd Meneide. std::to_underlying. 22 January 2021. URL: https://wg21.link/p1682r3