---
title: "Fixing std::bit_cast of types with padding bits"
document: P3969R2
date: 2026-06-28
audience: LEWG
reply-to:
  - "Jan Schultke <janschultke@gmail.com>"
---

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.



### Changes since R1

- Mentioned EWG support for the design in §3.1. Historical context for the design
- Mentioned active Clang Pull Request in §5. Implementation experience
- Fixed §6. Wording bug where bit-casting padding to `std::byte` was ill-formed
- Rebased §6. Wording on the post-Brno motions

### Changes since R0

- Limited the scope of the paper to only expand the *Mandates* of `std::bit_cast`
- Mentioned [[LWG4539]](https://wg21%2elink/lwg4539)
- Added §3.5. `union` edge case
- Expanded design discussion in various areas
- Rebased §6. Wording on the post-Croydon motions

## Introduction

> The following use of `std::bit_cast` has undefined behavior at compile time:
> 
> ```cpp
> 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]](https://eel.is/c++draft/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:
> 
> ```cpp
> // 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):
> 
> ```cpp
> // 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]](https://wg21%2elink/p3666r3)), considering that most `_BitInt` types (at least 7/8) have padding bits.

## Design

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

### Historical context for the design

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.
> 
> | SF | F | N | A | SA |
> | --- | --- | --- | --- | --- |
> | 6 | 6 | 1 | 0 | 0 |
> 
> **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):
> 
> | SF | F | N | A | SA |
> | --- | --- | --- | --- | --- |
> | 4 | 3 | 7 | 10 | 3 |
> 
> **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.

This option was also approved by EWG during the 2026-06 Brno meeting:

> EWG encourages LEWG to pursue the direction in P3969R1 of making a `std::bit_cast` on a type with padding ill-formed
> 
> | SF | F | N | A | SA |
> | --- | --- | --- | --- | --- |
> | 9 | 11 | 3 | 0 | 0 |
> 
> consensus.

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

- Since the byte values are now unspecified, UBSan (undefined behavior sanitizer) can no longer diagnose accessing/branching based on the upper 6 bytes as a bug. The bug (possibly CWE-908: Use of Uninitialized Resource) didn't go away, it just became non-conforming to diagnose it with termination.
- This approach should not work for constant evaluation because it would add non-determinism at compile time.

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](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.

### 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.

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

[[bit.cast] paragraph 4, bullet 2](https://eel.is/c++draft/bit.cast#4.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]](https://wg21%2elink/lwg4539), and can be resolved independently of this paper.

### 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.

### 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.

## 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.

## 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. However, there is an active Clang Pull Request at [https://github.com/llvm/llvm-project/pull/200362](https://github.com/llvm/llvm-project/pull/200362) which adds a `-Wbit-cast-padding` warning which triggers in exactly the same scenarios that this proposal would make ill-formed.

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`.

## Wording

The changes are relative to [[N5046]](https://wg21%2elink/n5046) with the changes from the post-Brno motions applied.

### [version.syn]

Bump the feature-test macro in [[version.syn]](https://eel.is/c++draft/version.syn) as follows:

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

### [bit.cast]

Change [[bit.cast]](https://eel.is/c++draft/bit.cast) as follows:

### Function template bit_cast [bit.cast]

```cpp
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`.

<ins>*Mandates*: 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`,</ins>

- <ins>si is in the value representation of s,</ins>
- <ins>si is enclosed by a union,</ins>
- <ins>di is not in the value representation of d,</ins>
- <ins>di is enclosed by a union, or</ins>
- <ins>di is enclosed by an object of unsigned ordinary character type or `std::byte` type.</ins>

> 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]](https://eel.is/c++draft/expr.const.core)).

*Returns*: An object of type `To`. Implicitly creates objects nested within the result ([[intro.object]](https://eel.is/c++draft/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]](https://eel.is/c++draft/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.

## 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.

### 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.

### 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.

### 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`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html#index-_005f_005fbuiltin_005fclear_005fpadding) and using an idiom such as:

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

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

#### 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"](https://eel.is/c++draft/dcl.init.general#def: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).

#### 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.

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

```cpp
__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`.

#### 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`:

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

## References

[N5046]

Thomas Köppe.

Working Draft, Programming Languages — C++

2026-05-12

https://open-std.org/jtc1/sc22/wg21/docs/papers/2026/n5046.pdf

[P0476R1]

JF Bastien.

Bit-casting object representations

2016-11-11

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0476r1.html

[P3666R3]

Jan Schultke.

Bit-precise integers

2026-02-21

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3666r3.html

[LWG4539]

Jan Schultke.

Undefined behavior in std::bit_cast should be diagnosed during constant evaluation

2026-03-10

https://wg21.link/lwg4539
