Implicit Conversions Between Integer Representation Types
Theme
Contents

Implicit Conversions Between Integer Representation Types

Implicit Conversions Between Integer Representation Types

Document #: P4408R0 Latest Status
Date: 2026-10-23
Project: Programming Language C++
Audience: SG6 Numerics
LEWG Library Evolution Working Group
Reply-to: Mateusz Pusz
<>

1 Revision history

1.1 R0

Initial revision. [P3045R9] listed [P0870R5] and [P2509R1] as papers that would help to prevent value truncation. [P3045R10] follows the rule of std::chrono::duration for conversions between integer representation types. This paper proposes to change it, and explains why it doesn’t build on either of those papers.

2 Introduction

Consider a distance stored in a 16-bit integer to save memory:

quantity<m, std::int16_t> d = 100'000 * m;

This compiles in [P3045R10], and d holds -31072 m. The library rejects a conversion that truncates a fraction, such as quantity<m, int> from 1.5 * m, and a unit conversion by a non-integral factor, such as quantity<km, int> from 1500 * m. It accepts the conversion above because it follows the rule of std::chrono::duration, which doesn’t compare integer types.

Quantities are often stored in small integer types. Sensor readings, register values, and fixed-point formats use 8-bit and 16-bit integers, and this is where such a wraparound does harm.

This paper proposes that a conversion between two integer representation types be implicit when, and only when, the destination can represent every value of the source on the implementation that compiles it. This is the answer that list initialization (9.5.5 List-initialization [dcl.init.list]) gives for narrowing from a non-constant expression of one integer type to another, and it needs no new type trait. The paper also describes a rule based on the integer conversion rank, which gives the same answer on more implementations, and explains why it is rejected.

3 Current design

In [P3045R10], implicitly_scalable decides whether a conversion from quantity<FromUnit, FromRep> to quantity<ToUnit, ToRep> is implicit:

template<unit auto FromUnit, typename FromRep, unit auto ToUnit, typename ToRep>
constexpr bool implicitly_scalable =
  treat_as_floating_point<ToRep> ||
  (!treat_as_floating_point<FromRep> && is_integral_scaling(FromUnit, ToUnit));

A conversion to a floating-point type is always implicit. A conversion between integer types is implicit when the unit changes by an integral factor, and the representation types play no other role. A user can specialize implicitly_scalable for a representation type that needs another rule, and a conversion that isn’t implicit has to be written with value_cast or with a rounding policy.

A unit conversion that overflows the destination, such as quantity<mm, std::int16_t> from std::int16_t{100} * m, is a separate question, and this paper doesn’t change it.

4 Existing practice

4.1 std::chrono

duration<Rep2, Period2> converts implicitly to duration<Rep, Period> when treat_as_floating_point_v<Rep> is true, or when the period ratio is integral and treat_as_floating_point_v<Rep2> is false (30.5.2 Constructors [time.duration.cons]). The integer types themselves are not compared, so these conversions are all implicit:

static_assert(std::is_convertible_v<duration<int>, duration<short>>);
static_assert(std::is_convertible_v<duration<long long>, duration<int>>);
static_assert(std::is_convertible_v<duration<int>, duration<unsigned>>);

duration<std::int16_t> d = duration<int>{100'000};  // d.count() == -31072 where int has 32 bits

4.2 Au

[Au] 0.6.1 forbids a conversion from a floating-point to an integer representation, and a unit conversion whose factor makes overflow likely for a small representation. A change of the representation alone isn’t checked. Each of these compiles, and GCC diagnoses none of them by default:

Quantity<Meters, std::int32_t> a = meters(std::int64_t{1});
Quantity<Meters, std::int16_t> b = meters(100'000);  // b.in(meters) == -31072
Quantity<Meters, unsigned>     c = meters(-1);

4.3 mp-units

[mp-units] 2.x implements the rule of Current design, and accepts the std::int16_t example of the Introduction with the same result.

Its safe_int<T> representation tests whether every value of the source type fits in the destination type, using the ranges of the two types. It uses the answer to skip a runtime range check that can’t fail, and to decide whether its converting constructor is explicit, so safe_int<int> from safe_int<long> is implicit on Windows and explicit on 64-bit Linux. This paper proposes the same answer for conversions between integer representation types of quantity.

5 What the rule has to do

  1. Reject a conversion between integer representation types that can change the value. std::int16_t from int is the case that corrupts data in the Introduction.
  2. Keep integer to floating-point conversions implicit. quantity<m> d = 1 * m; converts quantity<m, int> to quantity<m, double>, and double is the default representation type. Rejecting it would break a very common conversion in the library, and std::chrono makes it implicit too.
  3. Need nothing that the standard doesn’t have. The rule has to be specifiable in [P3045R10] without waiting for another proposal.

A fourth property divides the alternatives below: whether the same code may compile on one implementation and not on another. Alternatives explains why every rule that meets requirement 1 depends on the implementation, and The choice why this paper accepts it.

6 Alternatives

The alternatives split in two. A rule that, like the one of std::chrono, asks only whether a representation type is a floating-point one gives the same answer on every implementation. A rule that meets requirement 1 can’t, because it has to compare the integer types, and common type names denote different types on different implementations (see Drawbacks of every non-portable rule). This holds even for the strictest such rule, which makes every conversion between two distinct integer types explicit: long long and std::int64_t are the same type on some implementations and distinct types on others. That rule also rejects more conversions between integer types than any rule below.

6.1 The portable rule of std::chrono

This rule meets requirements 2 and 3, gives the same answer on every implementation, and it is already familiar to anyone who uses std::chrono. It accepts std::int16_t from int, so it fails requirement 1.

6.2 Non-portable rules

Each rule below meets requirement 1. Drawbacks of every non-portable rule describes what they have in common.

6.2.1 A narrowing trait, P0870

[P0870R8] proposes a trait that tells whether a conversion is narrowing in the sense of list initialization (9.5.5 List-initialization [dcl.init.list]). In 2026, LEWG voted to forward it to LWG for C++29, to be confirmed by an electronic poll.

The trait rejects std::int16_t from int. It doesn’t meet requirement 3 yet, although it is close to being part of the standard.

The trait fails requirement 2: a conversion from an integer type to a floating-point type is narrowing unless the source is a constant expression whose value converts exactly, and a type trait sees no value. quantity<m> d = 1 * m; would need value_cast<double>(1 * m).

The Proposal uses its answer for integer pairs only and keeps treat_as_floating_point for the rest, without waiting for the trait.

6.2.2 A value-preservation trait, P2509

[P2509R1] proposes is_value_preserving_convertible<From, To>, which is true when every value of From is represented exactly in To. It relies on numeric_limits, and its motivation cites [P2980R1], the proposed quantities and units library, as a use case.

Its answer depends on the implementation by design. The paper says so, and gives long double and double under two ABIs as an example.

It fails requirements 2 and 3:

6.2.3 Integer conversion rank

A conversion between two integer types is implicit when both are signed or both are unsigned and the integer conversion rank of To isn’t lower than that of From, or when To is the type that From promotes to (7.3.7 Integral promotions [conv.prom]). A rank that isn’t lower implies a width that isn’t smaller (6.9.7 Conversion ranks [conv.rank]), so the rule accepts only conversions that preserve every value. The promotion case is needed because the arithmetic operators convert both operands to the type of the result:

auto c = std::uint8_t{10} * m + std::uint8_t{20} * m;  // error without the promotion case: the sum is an int quantity,
                                                       // and int from std::uint8_t changes the signedness

The traits of [P0870R8] and [P2509R1] ask whether To represents every value of From on the implementation at hand, and the rank rule asks whether the ranks of the two types guarantee it. Every conversion between integer types that the rank rule accepts, the traits accept too, but not the other way round: the rank rule rejects int from long also where both have 32 bits, and long from std::uint16_t everywhere. No standard trait exposes the rank. For the standard integer types, an implementation can find it by listing them.

The rule isn’t portable. Its answer depends on the implementation in three ways. The type that a type of lower rank than int promotes to is int or unsigned int, depending on the widths. Which extended integer types exist is implementation-defined, and so is the relative rank of two of them with the same width. And the names and expressions listed in Drawbacks of every non-portable rule have different types on different implementations.

It meets requirements 1, 2, and 3. Rank or width compares it with the rule of The widths of the types.

6.2.4 The widths of the types

A conversion between two integer types is implicit when the destination represents every value of the source on the implementation that compiles it:

template<typename From, typename To>
constexpr bool integer-value-preserving =
  !(is_integral_v<From> && is_integral_v<To>) ||
  ((is_unsigned_v<From> || is_signed_v<To>) &&
   numeric_limits<To>::digits >= numeric_limits<From>::digits);

numeric_limits<T>::digits is the width of an integer type less its sign bit, so the rule needs nothing outside the standard, and it is what [P0870R8] and [P2509R1] answer for integer pairs. Its answer is the one that list initialization already gives: int i{l}; for a non-constant long l is well-formed on Windows and ill-formed on 64-bit Linux. The type that an integer type promotes to always represents every value of it, so the rule needs no special case for promotion. It meets requirements 1, 2, and 3.

6.2.5 Drawbacks of every non-portable rule

Whether a conversion is implicit decides whether a program is well-formed, and which overloads are viable. Under a non-portable rule, both can change when the program moves to another implementation, because type names, and the types of some expressions, denote different types on different implementations: the <cstdint> aliases, std::size_t, integer literals, and the results of integer promotion. std::int64_t is long on 64-bit Linux and long long on Windows.

void f(quantity<m, std::uint16_t> a, quantity<m, std::uint16_t> b)
{
  quantity<m, int> sum = a + b;      // OK where int is wider than 16 bits (the sum is int), error where it has 16 (sum is unsigned)
  quantity<m, int> q = 100'000 * m;  // OK where int is wider than 16 bits, error where it has 16 (the literal is long)
}

6.2.6 Rank or width

Both rules reject every conversion between integer types that can change a value on the implementation that compiles it. They differ in which other conversions they reject, and in how far the answer depends on the implementation.

6.2.6.1 Portability

The rank rule depends on the implementation only in the cases listed in Integer conversion rank. The width rule depends on it for every pair of types whose relative widths differ between implementations.

void f(quantity<m, unsigned short> us, quantity<m, long> l)
{
  quantity<m, int> a = us;  // both: OK where int is wider than short, error where they have the same width
  quantity<m, int> b = l;   // rank: error everywhere
                            // width: OK on Windows, error on 64-bit Linux
}

6.2.6.2 Conversions that preserve every value

The width rule accepts every conversion that preserves every value on the implementation at hand. The rank rule rejects some of them. A conversion from an unsigned type to a signed type of higher rank other than the type it promotes to, such as short from unsigned char, is rejected, because a higher rank doesn’t guarantee a strictly greater width (both may have 16 bits where CHAR_BIT is 16).

void f(quantity<m, unsigned> u, quantity<m, std::uint16_t> u16, quantity<m, unsigned char> uc)
{
  quantity<m, long> b = u;    // rank: error everywhere      width: OK where long is wider than unsigned
  quantity<m, long> c = u16;  // rank: error everywhere      width: OK everywhere
  quantity<m, short> d = uc;  // rank: error everywhere      width: OK where short is wider than unsigned char
}

6.2.6.3 The choice

This paper proposes the width rule. It rejects fewer conversions, and each conversion it rejects can change a value on the implementation that compiles it. So an explicit conversion between integer representation types is needed only where a value can change, and it keeps telling a reader so. The rank rule also requires explicit conversions where no value can change, and a user who meets enough of them may write them by habit. Where the answer depends on the implementation, as for int from long, the width rule requires the explicit conversion on the implementation where the value can change, which is the one that has to be reviewed when the code is ported.

List initialization made the same choice: whether int i{l}; is narrowing follows the widths of int and long. The advantage of the rank rule, the same answer on every implementation for most pairs of standard integer types, doesn’t extend to the <cstdint> aliases, which code that cares about the widths of its integers often uses.

7 Proposal

Change the default of implicitly_scalable in [P3045R10] to:

template<unit auto FromUnit, typename FromRep, unit auto ToUnit, typename ToRep>
constexpr bool implicitly_scalable =
  treat_as_floating_point<ToRep> ||
  (!treat_as_floating_point<FromRep> && is_integral_scaling(FromUnit, ToUnit) &&
   integer-value-preserving<FromRep, ToRep>);

where integer-value-preserving is the exposition-only variable template of The widths of the types.

implicitly_scalable constrains the converting constructor of quantity, as the corresponding constraint does for std::chrono::duration (30.5.2 Constructors [time.duration.cons]), so the rule applies to every conversion between quantities: initialization, assignment, compound assignment, function arguments and return values, and the conversion of the operands of +, -, and % to their common quantity type. A conversion that it rejects is written with value_cast, such as value_cast<short>(q).

bool and the character types char, wchar_t, char8_t, char16_t, and char32_t are integer types too, but [P3045R10] doesn’t accept them as representation types, so the rule doesn’t apply to them. signed char and unsigned char are a signed and an unsigned integer type (6.9.3 Fundamental types [basic.fundamental]) that programs use for small numbers. They remain valid representation types, so std::int8_t and std::uint8_t are covered. The rule covers pairs of integer types only, and a wrapper such as safe_int<int> keeps specializing implicitly_scalable. A type that is_integral doesn’t recognize, such as __int128 in a strict standard mode, keeps the current rule unless implicitly_scalable is specialized for it.

[mp-units] implements the proposed rule on its version 3 line, which implements [P3045R10]. [mp-units] keeps the rule of std::chrono in its 2.x releases, because the change breaks existing code such as quantity<m, short> q = 42 * m;.

8 Costs

The rule rejects conversions that [P3045R10] accepts today. Every rule that meets requirement 1 has each of these costs on some implementation, and the rule of Integer conversion rank has them on more. The drawbacks that follow from the dependence on the implementation are listed in Drawbacks of every non-portable rule.

Each of these costs is a compile-time error. The conversion in the Introduction compiles and gives a wrong value at run time. Each of them also requires an explicit conversion that the user may know to be safe for the values at hand. A user who meets enough of them may write explicit conversions by habit, and then an explicit conversion no longer tells a reader where a value can change. The width rule keeps this to the conversions that can change a value of their source type.

9 Straw polls

  1. An implicit conversion between integer representation types that can change the value, such as quantity<m, std::int16_t> from 100'000 * m, should be rejected. This means leaving the portable rule of std::chrono for one of the non-portable rules: the same code may compile on one implementation and not on another.

  2. [P3045R10] should use the rule of The widths of the types (as proposed: implicit when the destination can represent every value of the source on the given implementation, so int from long is implicit on Windows and rejected on 64-bit Linux).

    [ Editor's note: Taken only if poll 1 has consensus. ]

10 Acknowledgments

Special thanks and recognition goes to The C++ Alliance for supporting Mateusz’s membership in the ISO C++ Committee and the production of this proposal.

The author also thanks Giuseppe D’Angelo for his work on detecting narrowing and value-changing conversions in [P0870R8] and [P2509R1].

11 References

[Au] The Au Units library.
https://aurora-opensource.github.io/au
[mp-units] mp-units - A Physical Quantities and Units library for C++.
https://mpusz.github.io/mp-units
[P0870R5] Giuseppe D’Angelo. 2023-02-15. A proposal for a type trait to detect narrowing conversions.
https://wg21.link/p0870r5
[P0870R8] Giuseppe D’Angelo. 2026-06-09. A proposal for a type trait to detect narrowing conversions.
https://wg21.link/p0870r8
[P2509R1] Giuseppe D’Angelo. 2025-05-19. A proposal for a type trait to detect value-preserving conversions.
https://wg21.link/p2509r1
[P2980R1] Mateusz Pusz, Dominik Berner, Johel Ernesto Guerrero Peña, Charles Hogg, Nicolas Holthaus, Roth Michaels, Vincent Reverdy. 2023-11-28. A motivation, scope, and plan for a quantities and units library.
https://wg21.link/p2980r1
[P3045R10] Mateusz Pusz. Quantities and units library.
https://wg21.link/p3045r10
[P3045R9] Mateusz Pusz, Dominik Berner, Johel Ernesto Guerrero Peña, Charles Hogg, Nicolas Holthaus, Roth Michaels, Vincent Reverdy. 2026-06-13. Quantities and units library.
https://wg21.link/p3045r9