Fixing std::bit_cast of types
with padding bits

Document number:
P3969R1
Date:
2026-05-11
Audience:
LEWG, EWG
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
Reply-to:
Jan Schultke <janschultke@gmail.com>
GitHub Issue:
wg21.link/P3969/github
Source:
github.com/eisenwave/cpp-proposals/blob/master/src/bit-cast-padding.cow

When bit-casting a type containing padding bits to a type with no padding bits, std::bit_cast degenerates into an alternative spelling for std::unreachable (some exceptions apply). To prevent this inadvertent footgun, this proposal makes that degenerate form of std::bit_cast ill-formed.

Contents

1

Revision history

1.1

Changes since R0

2

Introduction

3

Design

3.1

Can't you make std::bit_cast produce unspecified or erroneous values?

3.2

Constraints vs Mandates

3.3

Requiring std::bit_cast UB to be diagnosed in constant expressions

3.4

union edge case

3.5

Future direction

4

Impact on existing code

5

Implementation experience

6

Wording

6.1

[version.syn]

6.2

[bit.cast]

7

Appendix: Single-function solution vs. two-function solution

7.1

Advantages of the two-function solution

7.2

Advantages of the single-function solution

7.3

Can't you clear padding bits before bit-casting?

7.3.1

Padding bits are finicky

7.3.2

No padding bits during constant evaluation

7.3.3

std::clear_padding is not ergonomic for bit-casting

7.3.4

std::clear_padding is less capable

8

References

1. Revision history

1.1. Changes since R0

2. Introduction

The following use of std::bit_cast has undefined behavior at compile time:

constexpr auto x = std::bit_cast<__int128>(0.0L); // GCC accepts (x = 0), Clang rejects

That is because an 80-bit x87 long double has 6 bytes of padding, and it is undefined behavior to map those padding bits onto non-padding bits in the destination type via std::bit_cast. [bit.cast] does not disqualify this use of std::bit_cast from being a constant expression.

Surprisingly, the undefined behavior in such cases does not depend on the argument. A specialization std::bit_cast<To, From> is an alternative spelling for std::unreachable if From has padding bits and To does not, a degenerate form. Despite not depending on the argument, the degenerate form of std::bit_cast does not violate the Constraints or Mandates element, leaving the bug undetected. Compilers also have no warning for the degenerate form at the time of writing.

This behavior is a footgun, and is not very useful. If users wanted a function that always has UB, they should be writing std::unreachable, not std::bit_cast.

If the padding bits in From are all mapped onto std::byte or unsigned char objects within To, the behavior is well-defined.

That makes it possible to implement a proper conversion from long double to __int128, although it requires multiple steps:

// OK because indeterminate bits go into unsigned char: auto bytes = std::array<unsigned char, 16>(0.0L); for (int i : { 10, 11, 12, 13, 14, 15 }) bytes[i] = 0; auto result = std::bit_cast<__int128>(bytes);

Another possible workaround is to use a struct containing a __int128 x:80 bit-field, under the assumption those 80 bits line up with those in long double.

The GCC behavior in the example above is more accurately explained by long double having no padding bytes. From a C++ standard perspective, GCC's long double is a type with no padding bytes, but six upper bytes that are always zero (assuming any of this behavior is intentional and not just a compiler bug):

// OK: uppermost byte is not a padding byte, but is zero static_assert(bit_cast<array<unsigned char, 16>>(0.0L)[15] == 0); // UB: forms a long double whose value representation is not valid for the type constexpr auto f = bit_cast<long double>(__int128(-1)); // Passes on GCC, despite bit_cast having set all bytes of f to -1, // and despite long double having no padding bytes judging by the previous assertion: static_assert(bit_cast<array<unsigned char, 16>>(f)[15] == 0);

GCC compiles this code; Clang already rejects both assertions.

Another case where the degenerate form may arise frequently is bit-casting _BitInt (supported by Clang as an extension and proposed in [P3666R3]), considering that most _BitInt types (at least 7/8) have padding bits.

3. Design

The proposed solution is to make the degenerate form of std::bit_cast ill-formed.

However, R0 of this paper was more ambitious and presented two approaches:

  1. Make the degenerate form of std::bit_cast ill-formed. Also add a new std::bit_cast_zero_padding function which treats padding bits in the source as zero instead of as indeterminate. Other than that, this new function has the same behavior as std::bit_cast.
  2. Make std::bit_cast behave like std::bit_cast_zero_padding without adding any new function. This should be done as a DR against C++20.

LEWG and EWG gave contradictory feedback on which approach to choose. During the 2026-03-10 LEWG telecon, the following poll was taken:

POLL: We think that changing the current undefined behaviour of bit_cast to silently zeroing padding bits is problematic.

SFFNASA
66100

Attendance: 19
Author's position: SF
Outcome: Consensus in favor.

When EWG later saw the paper during the 2026-03 Croydon meeting, the following poll was taken:

EWG prefers adding another flavor of bit_cast (padding zero-ing) in addition to existing bit_cast (which makes reading padding bits a library UB):

SFFNASA
437103

Result: not consensus

If LEWG does not want to silently change the behavior of std::bit_cast and EWG does not want another function, then the only option is to make the degenerate form of std::bit_cast ill-formed without adding any new function.

3.1. Can't you make std::bit_cast produce unspecified or erroneous values?

A possible approach would be to make std::bit_cast produce unspecified bit values instead of indeterminate bit values. That is, std::bit_cast<__int128>(0.0L) would create a __int128 with 10 predictable bytes and 6 bytes with unspecified value. There are two problems with this idea:

Overall, this design sweeps the problem under the rug with little benefit to the user. However, it does enable certain uses where std::bit_cast is producing a type that is used like a bag of bits but isn't a byte array.

C APIs often accept void* as extra data, so it would be convenient to be able to bit-cast a padded type to void* without UB. This case is explained in more detail at https://github.com/eisenwave/cpp-proposals/issues/175

It is also possible to make the result have erroneous value. However, once again, this approach could not be used to portably bit-cast long double to __int128, especially not during constant evaluation; the degenerate form of std::bit_cast would then always produce erroneous values, so it makes no sense to let it compile in the first place. This solution would only benefit the case of bit-casting to a byte array; perhaps that is worth pursuing, but the only way not to add cost to std::bit_cast (with no opt-out) would be to give the bytes an unspecified value that is considered an erroneous value. This provides minimal (if any) benefit, and could be explored in a separate paper; it is a separate issue from the one presented in this paper.

3.2. Constraints vs Mandates

The degenerate form of std::bit_cast should be diagnosed using a Mandates element (that is, static_assert). That is because the condition for the degenerate form is relatively complicated and may change in the future. Also, Constraints tempts the user to test whether bit_cast is safe using requires, but this test can have false positives. The detection of the degenerate form would only tell the user whether all possible arguments result in undefined behavior.

Conceptually, Constraints for std::bit_cast should tell the user whether bit-casting is technically feasible due to sizes matching and types being trivially copyable, whereas Mandates should catch misuses such as passing consteval-only types or types that result in the degenerate form.

Also, using Mandates makes the implementation strategy simpler; it only requires a modification of the __builtin_bit_cast intrinsic.

3.3. Requiring std::bit_cast UB to be diagnosed in constant expressions

[bit.cast] paragraph 4, bullet 2 explicitly makes indeterminate result bits undefined behavior inside std::bit_cast, which arguably makes it library UB, which is generally not required to be diagnosed during constant evaluation. Another undiagnosed UB is forming invalid value representations, such as in the case of bit_cast<bool>(char(2)) when 2 is not a valid representation of bool.

During the 2026-03-10 LEWG telecon, LEWG expressed unanimously that UB in std::bit_cast should be diagnosed during constant evaluation. This defect is addressed in [LWG4539], and can be resolved independently of this paper.

3.4. union edge case

When bit-casting bits that are enclosed by a union, it is unknown during translation whether the bits are padding bits or not. Similarly, when bit-casting padding bits and the corresponding bits in the destination are enclosed by a union, it is also unknown whether padding bits are mapped onto non-padding bits or not. Undefined behavior in those cases can only be caught by sanitizers; it cannot be diagnosed during translation.

The solution is to simply allow such cases (which is the status quo). Disallowing them would mean that std::bit_cast of a union would have to be ill-formed, breaking existing code.

3.5. Future direction

The proposed solution does not preclude the possibility of making std::bit_cast clear the padding bits in the future, nor does it preclude the possibility of adding a new function that does that, nor does it preclude the possibility of making the degenerate form of std::bit_cast produce unspecified or erroneous values instead of being ill-formed.

All doors are left open; we are merely preventing unnecessary bugs.

4. Impact on existing code

The proposed change only makes the degenerate form of std::bit_cast ill-formed, i.e. it raises an error in code that contained unconditional UB.

5. Implementation experience

While compilers diagnose misuses of std::bit_cast during constant evaluation, the proposed compile-time check has not been implemented in any compiler at the time of writing. The most straight-forward implementation strategy is to add a check in the compiler's __builtin_bit_cast intrinsic for the degenerate form of std::bit_cast.

6. Wording

The changes are relative to [N5032] with the changes from the post-Croydon motions applied.

[version.syn]

Bump the feature-test macro in [version.syn] as follows:

#define __cpp_lib_bit_cast 201806L 20XXXXL // freestanding, also in <bit>

[bit.cast]

Change [bit.cast] as follows:

Function template bit_cast [bit.cast]

template<class To, class From> constexpr To bit_cast(const From& from) noexcept;

Constraints:

  • sizeof(To) == sizeof(From) is true;
  • is_trivially_copyable_v<To> is true;
  • is_trivially_copyable_v<From> is true.

Mandates:

  • Neither To nor From are consteval-only types ([basic.types.general]).
  • For every bit si in the object representation of a complete object s of type From and the corresponding bit di in the object representation of a complete object d of type To,
    • si is in the value representation of s,
    • si is enclosed by a union,
    • di is not in the value representation of d, or
    • di is enclosed by a union.

The strategy is to isolate the case where we know with certainty that a padding bit is being mapped onto a non-padding bit.

It is important to say complete object because enclosed looks for an enclosing object recursively, and we cannot know if on some level s itself is enclosed by a union or not. We are imposing a requirement here that has to apply universally to any possible objects s and d.

Constant When: Neither To nor From has constexpr-unknown representation ([expr.const.core]).

Returns: An object of type To. Implicitly creates objects nested within the result ([intro.object]). Each bit of the value representation of the result is equal to the corresponding bit in the object representation of from. Padding bits of the result are unspecified. For the result and each object created within it, if there is no value of the object's type corresponding to the value representation produced, the behavior is undefined. If there are multiple such values, which value is produced is unspecified. A bit in the value representation of the result is indeterminate if it does not correspond to a bit in the value representation of from or corresponds to a bit for which the smallest enclosing object is not within its lifetime or has an indeterminate value ([basic.indet]). A bit in the value representation of the result is erroneous if it corresponds to a bit for which the smallest enclosing object has an erroneous value. For each bit b in the value representation of the result that is indeterminate or erroneous, let u be the smallest object containing that bit enclosing b:

  • If u is of unsigned ordinary character type or std::byte type, u has an indeterminate value if any of the bits in its value representation are indeterminate, or otherwise has an erroneous value.
  • Otherwise, if b is indeterminate, the behavior is undefined.
  • Otherwise, the behavior is erroneous, and the result is as specified above.

The result does not otherwise contain any indeterminate or erroneous values.

7. Appendix: Single-function solution vs. two-function solution

The following section covers the advantages and disadvantages of the two approaches presented in R0 of this paper, as introduced in §3. Design. This discussion is interesting historically, but unnecessary to understand the design of this proposal.

7.1. Advantages of the two-function solution

The single-function solution is problematic because std::bit_cast can be used to convert padded types to a byte array without undefined behavior and with zero overhead. Wiping padding bits would add more cost to existing code. With only a single function, there is also no way to opt out of that cost other than using std::memcpy instead, and that only works outside of constant evaluation.

Furthermore, if users assumed std::bit_cast to clear padding, they may inadvertently access uninitialized memory on older compiler versions, where that behavior is not implemented yet. Perfectly well-defined C++29 code with no erroneous behavior that uses std::bit_cast could be copied and pasted into older code bases, and suddenly obtain undefined behavior.

Last but not least, users may be surprised by std::bit_cast changing the value of any bits. Conceptually, it is a reinterpretation of existing bits as a new type, and it is desirable to express behavior like zeroing of padding explicitly. This surprising behavior may also sweep developer mistakes under the rug; bit-casting a padded type to an unpadded type may happen unintentionally, and if it was diagnosed, it would inform the user about incorrect assumptions. That often seems more desirable than just zeroing the padding bits and thus silencing any problems.

7.2. Advantages of the single-function solution

The obvious benefit of changing the behavior of std::bit_cast is that existing UB in users' code disappears, without any refactoring effort. This would especially be the case if the proposal is treated as a DR against C++20.

Additionally, some may argue that std::bit_cast_zero_padding should be the default anyway, considering that it's safer to use.

The single-function solution is also easier to implement; it only requires a single __builtin_bit_cast intrinsic to be maintained.

7.3. Can't you clear padding bits before bit-casting?

In the discussion of this proposal prior to publication, it was suggested to clear the padding before bit-casting. That is, standardizing __builtin_clear_padding and using an idiom such as:

long double x = /* ... */; std::clear_padding(x); std::bit_cast<__int128>(x);

However, there are severe problems with this aproach, explained below.

7.3.1. Padding bits are finicky

There are only a few places in the standard where padding bits receive a useful value. For example, zero-initialization is also stated to result in padding bits being zeroed ([dcl.init.general] definition of "zero-initialization"). In most scenarios (e.g. local variables), the padding bits have erroneous or indeterminate value. Even when the padding bits have defined value, lvalue-to-rvalue conversion does not propagate padding bits, and the assignment operator may render them indeterminate or erroneous.

This makes it highly questionable to access padding bits and rely on them having any specific value. If the user forgets to write std::clear_padding or falsely assumes that padding bits are already cleared, they could easily acccess uninitialized memory (which may be a security vulnerability).

7.3.2. No padding bits during constant evaluation

Besides the safety issues, the approach of clearing padding bits in the object does not make any sense for constant evaluation. For instance, Clang does not store an object representation for values during constant evaluation. When bit-casting, one is generated on the fly.

This would likely mean that constexpr std::clear_padding is effectively unimplementable in current compilers.

7.3.3. std::clear_padding is not ergonomic for bit-casting

We typically pass large types by reference, even if they are trivially copyable. Assuming we want to cast a type BigT to another type BigU while clearing padding, the procedure has a lot of steps:

__int128 cast(long double x) { // 1. Clear padding. std::clear_padding(x); // 2. Create a variable for holding the result. __int128 result; // 3. Use std::memcpy to convert the bits. // This is necessary because std::bit_cast ignores the values of // padding bits in the original, so even though we've cleared them, // they would not be propagated. std::memcpy(&result, &x, sizeof(__int128)); // 4. Return the result. return result; }

This procedure gets even more complicated when we receive a const& or operate on a std::span<const T>, in which case we need to create a temporary variable that we can mutate with std::clear_padding.

Regardless, this procedure is fairly complex compared to using a std::bit_cast_zero_padding function that does it all in one go. All of that complexity yields no advantage; even if std::clear_padding was constexpr, std::memcpy isn't, so cast cannot be made constexpr.

7.3.4. std::clear_padding is less capable

Last but not least, std::clear_padding is strictly less capable than std::bit_cast_zero_padding because std::clear_padding (at least with current compiler technology) is not a viable solution during constant evaluation. However, std::clear_padding can be implemented in terms of std::bit_cast_zero_padding:

template <typename T> void clear_padding(T& object) { // 1. Convert to a byte array. // All the input padding bits are cleared, // and there are not padding bits in a byte array. auto zeroed = std::bit_cast_zero_padding<std::array<unsigned char, sizeof(T)>>(object); // 2. Copy the bytes back into the object. // The bits in the value representation have not been changed, // so this does not change the value of T, only the values of padding bits. std::memcpy(&object, &zeroed, sizeof(T)); }

8. References

[N5032] Thomas Köppe. Working Draft, Programming Languages — C++ 2025-12-15 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/n5032.pdf
[P0476R1] JF Bastien. Bit-casting object representations 2016-11-11 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0476r1.html
[LWG4539] Jan Schultke. Undefined behavior in std::bit_cast should be diagnosed during constant evaluation 2026-03-10 https://wg21.link/lwg4539