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
degenerates into an alternative spelling for
(some exceptions apply).
To prevent this inadvertent footgun,
this proposal makes that degenerate form of ill-formed.
Contents
Revision history
Changes since R0
Introduction
Design
Can't you make std :: bit_cast produce unspecified or erroneous values?
Constraints vs Mandates
Requiring std::bit_cast UB to be diagnosed in constant expressions
union edge case
Future direction
Impact on existing code
Implementation experience
Wording
[version.syn]
[bit.cast]
Appendix: Single-function solution vs. two-function solution
Advantages of the two-function solution
Advantages of the single-function solution
Can't you clear padding bits before bit-casting?
Padding bits are finicky
No padding bits during constant evaluation
std :: clear_padding is not ergonomic for bit-casting
std :: clear_padding is less capable
References
1. Revision history
1.1. Changes since R0
- Limited the scope of the paper to only expand the Mandates of
std :: bit_cast - Mentioned [LWG4539]
- Added §3.4.
edge caseunion - Expanded design discussion in various areas
- Rebased §6. Wording on the post-Croydon motions
2. Introduction
has undefined behavior at compile time:
That is because an 80-bit x87 has 6 bytes of padding,
and it is undefined behavior to map those padding bits onto non-padding bits
in the destination type via .
[bit.cast] does not disqualify this use of
from being a constant expression.
Surprisingly, the undefined behavior in such cases does not depend on the argument.
A specialization is an alternative spelling for
if has padding bits and does not,
a degenerate form.
Despite not depending on the argument,
the degenerate form of 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 ,
not .
are all mapped onto
or objects within ,
the behavior is well-defined.
That makes it possible to implement a proper conversion from
to , although it requires multiple steps:
Another possible workaround is to use a
containing a bit-field,
under the assumption those 80 bits line up with those in .
having no padding bytes.
From a C++ standard perspective, GCC's
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):
GCC compiles this code; Clang already rejects both assertions.
(supported by Clang as an extension and proposed in [P3666R3]),
considering that most types (at least 7/8) have padding bits.
3. Design
The proposed solution is to make the degenerate form of ill-formed.
However, R0 of this paper was more ambitious and presented two approaches:
-
Make the degenerate form of
ill-formed. Also add a newstd :: bit_cast function which treats padding bits in the source as zero instead of as indeterminate. Other than that, this new function has the same behavior asstd :: bit_cast_zero_padding .std :: bit_cast -
Make
behave likestd :: bit_cast without adding any new function. This should be done as a DR against C++20.std :: bit_cast_zero_padding
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
and EWG does not want another function,
then the only option is to make the degenerate form of 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 produce
unspecified bit values instead of indeterminate bit values.
That is, would create a
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 is producing a
type that is used like a bag of bits but isn't a byte array.
as extra data,
so it would be convenient to be able to bit-cast a padded type to 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 to ,
especially not during constant evaluation;
the degenerate form of 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 (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 should be diagnosed using
a Mandates element (that is, ).
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
is safe
using ,
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
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 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 ,
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
when is not a valid representation of .
During the 2026-03-10 LEWG telecon,
LEWG expressed unanimously that UB in
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 of a
would have to be ill-formed,
breaking existing code.
3.5. Future direction
The proposed solution does not preclude the possibility
of making 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
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 ill-formed,
i.e. it raises an error in code that contained unconditional UB.
5. Implementation experience
While compilers diagnose misuses of 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 intrinsic
for the degenerate form of .
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:
[bit.cast]
Change [bit.cast] as follows:
Function template bit_cast [bit.cast]
Constraints:
issizeof ( To ) == sizeof ( From ) ;true isis_trivially_copyable_v < To > ;true isis_trivially_copyable_v < From > .true
Mandates:
- Neither
norTo are consteval-only types ([basic.types.general]).From -
For every bit in the object representation
of a complete object of type
and the corresponding bit in the object representation of a complete object of typeFrom ,To - is in the value representation of ,
- is enclosed by a union,
- is not in the value representation of , or
- is enclosed by a union.
It is important to say complete object
because enclosed
looks for an enclosing object recursively,
and we cannot know if on some level itself is enclosed by a union or not.
We are imposing a requirement here that has to apply universally to any possible objects
and .
Constant When:
Neither nor
has constexpr-unknown representation ([expr.const.core]).
Returns:
An object of type .
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 .
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
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 in the value representation of the result
that is indeterminate or erroneous,
let be the smallest object containing that bit enclosing :
-
If is of unsigned ordinary character type or
type, has an indeterminate value if any of the bits in its value representation are indeterminate, or otherwise has an erroneous value.std :: byte - Otherwise, if 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
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 instead,
and that only works outside of constant evaluation.
Furthermore, if users assumed 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 could be copied and pasted into older code bases,
and suddenly obtain undefined behavior.
Last but not least,
users may be surprised by 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
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 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 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
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 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 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 to another type
while clearing padding,
the procedure has a lot of steps:
This procedure gets even more complicated when we receive a
or operate on a ,
in which case we need to create a temporary variable that we can mutate
with .
Regardless, this procedure is fairly complex compared to using a
function that does it all in one go.
All of that complexity yields no advantage;
even if was ,
isn't,
so cannot be made .
7.3.4. std :: clear_padding is less capable
Last but not least, is strictly less capable
than
because (at least with current compiler technology)
is not a viable solution during constant evaluation.
However, can be implemented
in terms of :