[libcxx-commits] [libcxx] [libc++] std::byteswap support for _BitInt(N) (PR #196512)

Xavier Roche via libcxx-commits libcxx-commits at lists.llvm.org
Fri May 8 04:24:34 PDT 2026


https://github.com/xroche created https://github.com/llvm/llvm-project/pull/196512

Add a byte-reversal loop fallback for `std::byteswap` when `sizeof(T) > 16`,
so the function works for `_BitInt(N)` with `N > 128` and any future wider
integer type. Without it, those calls hit `static_assert(sizeof(_Tp) == 0)`
and fail to compile.

Reject `_BitInt(N)` where `N` is not a multiple of `CHAR_BIT`. The existing
`__builtin_bswap{16,32,64,128}` paths swap the storage representation
including padding bits, and the resulting value's meaning is unspecified.
A new `static_assert` catches that case and reports it. Size-1 types are
exempt from the check, since no bytes move there.

Part of the [_BitInt(N) libc++ effort](https://discourse.llvm.org/t/bitint-n-support-in-libc-investigations-possible-improvements-looking-for-guidance/90063).

Assisted-by: Claude (Anthropic)

>From a5cf19d74d4713aa27468be7ecbefcfb090ff111 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Fri, 8 May 2026 13:15:13 +0200
Subject: [PATCH 1/3] [libc++] Add generic byteswap for wide integral types

Replace the static_assert(sizeof(_Tp) == 0) failure for sizeof > 16 with
a byte-reversal loop. std::byteswap now works for _BitInt(N) with N > 128
and any future wider integer type. The loop reads the value 8 bits at a
time via right-shift + cast through unsigned char, and writes the bytes
back in reverse order. Left-shift and bitwise-OR on signed integral types
are well-defined modulo 2^width in C++23.

For sizeof 1, 2, 4, 8, and 16 the existing __builtin_bswap* paths are
unchanged.

Part of the [_BitInt(N) libc++ effort](https://discourse.llvm.org/t/bitint-n-support-in-libc-investigations-possible-improvements-looking-for-guidance/90063).

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 libcxx/include/__bit/byteswap.h | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/libcxx/include/__bit/byteswap.h b/libcxx/include/__bit/byteswap.h
index 7ce7e069b4142..66a99713bd0dd 100644
--- a/libcxx/include/__bit/byteswap.h
+++ b/libcxx/include/__bit/byteswap.h
@@ -12,6 +12,8 @@
 
 #include <__concepts/arithmetic.h>
 #include <__config>
+#include <__cstddef/size_t.h>
+#include <climits>
 #include <cstdint>
 
 #if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
@@ -42,7 +44,16 @@ template <integral _Tp>
 #    endif // __has_builtin(__builtin_bswap128)
 #  endif   // _LIBCPP_HAS_INT128
   } else {
-    static_assert(sizeof(_Tp) == 0, "byteswap is unimplemented for integral types of this size");
+    // Generic byte-reversal for wide integer types (e.g. _BitInt(N) with
+    // N > 128). Reads the value 8 bits at a time and writes the bytes
+    // back in reverse order. Left-shift on signed integral types is
+    // well-defined modulo 2^width since C++20.
+    _Tp __result = 0;
+    for (size_t __i = 0; __i < sizeof(_Tp); ++__i) {
+      __result |= static_cast<_Tp>(static_cast<unsigned char>(__val >> (__i * CHAR_BIT)))
+               << ((sizeof(_Tp) - 1 - __i) * CHAR_BIT);
+    }
+    return __result;
   }
 }
 

>From ad298d82fb859114b20ef52b1b8f63fc79a67299 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Fri, 8 May 2026 13:15:44 +0200
Subject: [PATCH 2/3] [libc++] Reject byteswap of types with padding bits

For _BitInt(N) where N is not a multiple of CHAR_BIT, byteswap of the
storage representation moves padding bits into significant positions and
produces a value whose meaning is unspecified. Reject those cases with a
static_assert that fires when the type's value bits do not fill the
entire object representation.

The size-1 case is exempt from the check because no bytes move and no
padding gets relocated. bool, _BitInt(7), and similar types stay
identity.

Suggested by philnik in the libc++ _BitInt review thread ("byteswap
with padding: should definitely be rejected").

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 libcxx/include/__bit/byteswap.h | 56 +++++++++++++++++++++------------
 1 file changed, 36 insertions(+), 20 deletions(-)

diff --git a/libcxx/include/__bit/byteswap.h b/libcxx/include/__bit/byteswap.h
index 66a99713bd0dd..2754265080a34 100644
--- a/libcxx/include/__bit/byteswap.h
+++ b/libcxx/include/__bit/byteswap.h
@@ -15,6 +15,7 @@
 #include <__cstddef/size_t.h>
 #include <climits>
 #include <cstdint>
+#include <limits>
 
 #if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
 #  pragma GCC system_header
@@ -27,33 +28,48 @@ _LIBCPP_BEGIN_NAMESPACE_STD
 template <integral _Tp>
 [[nodiscard]] _LIBCPP_HIDE_FROM_ABI constexpr _Tp byteswap(_Tp __val) noexcept {
   if constexpr (sizeof(_Tp) == 1) {
+    // Identity for size-1 types: no bytes move and no padding gets shuffled
+    // into significant positions. bool, char, and _BitInt(N <= CHAR_BIT)
+    // all land here.
     return __val;
-  } else if constexpr (sizeof(_Tp) == 2) {
-    return __builtin_bswap16(__val);
-  } else if constexpr (sizeof(_Tp) == 4) {
-    return __builtin_bswap32(__val);
-  } else if constexpr (sizeof(_Tp) == 8) {
-    return __builtin_bswap64(__val);
+  } else {
+    // Reject types whose value bits do not fill the entire object
+    // representation (e.g. _BitInt(13) has 3 padding bits in 2 bytes of
+    // storage). The byte-level builtins below would swap those padding
+    // bits into significant positions, and the resulting value's meaning
+    // is unspecified. The size-1 case above is exempt because no bytes
+    // move.
+    static_assert(numeric_limits<_Tp>::digits + numeric_limits<_Tp>::is_signed == sizeof(_Tp) * CHAR_BIT,
+                  "std::byteswap requires a type whose value bits fill the entire "
+                  "object representation; types like _BitInt(N) where N is not a "
+                  "multiple of CHAR_BIT have padding bits and are rejected");
+    if constexpr (sizeof(_Tp) == 2) {
+      return __builtin_bswap16(__val);
+    } else if constexpr (sizeof(_Tp) == 4) {
+      return __builtin_bswap32(__val);
+    } else if constexpr (sizeof(_Tp) == 8) {
+      return __builtin_bswap64(__val);
 #  if _LIBCPP_HAS_INT128
-  } else if constexpr (sizeof(_Tp) == 16) {
+    } else if constexpr (sizeof(_Tp) == 16) {
 #    if __has_builtin(__builtin_bswap128)
-    return __builtin_bswap128(__val);
+      return __builtin_bswap128(__val);
 #    else
-    return (static_cast<_Tp>(byteswap(static_cast<uint64_t>(__val))) << 64) |
-           static_cast<_Tp>(byteswap(static_cast<uint64_t>(__val >> 64)));
+      return (static_cast<_Tp>(byteswap(static_cast<uint64_t>(__val))) << 64) |
+             static_cast<_Tp>(byteswap(static_cast<uint64_t>(__val >> 64)));
 #    endif // __has_builtin(__builtin_bswap128)
 #  endif   // _LIBCPP_HAS_INT128
-  } else {
-    // Generic byte-reversal for wide integer types (e.g. _BitInt(N) with
-    // N > 128). Reads the value 8 bits at a time and writes the bytes
-    // back in reverse order. Left-shift on signed integral types is
-    // well-defined modulo 2^width since C++20.
-    _Tp __result = 0;
-    for (size_t __i = 0; __i < sizeof(_Tp); ++__i) {
-      __result |= static_cast<_Tp>(static_cast<unsigned char>(__val >> (__i * CHAR_BIT)))
-               << ((sizeof(_Tp) - 1 - __i) * CHAR_BIT);
+    } else {
+      // Generic byte-reversal for wide integer types (e.g. _BitInt(N) with
+      // N > 128). Reads the value 8 bits at a time and writes the bytes
+      // back in reverse order. Left-shift on signed integral types is
+      // well-defined modulo 2^width since C++20.
+      _Tp __result = 0;
+      for (size_t __i = 0; __i < sizeof(_Tp); ++__i) {
+        __result |= static_cast<_Tp>(static_cast<unsigned char>(__val >> (__i * CHAR_BIT)))
+                 << ((sizeof(_Tp) - 1 - __i) * CHAR_BIT);
+      }
+      return __result;
     }
-    return __result;
   }
 }
 

>From 6e678d46cbf6a0aeca45dcc50b0e16a579636ba6 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Fri, 8 May 2026 13:16:06 +0200
Subject: [PATCH 3/3] [libc++][test] Add std::byteswap tests for _BitInt(N)

Two test files for the changes in the prior two commits.

byteswap.bitint.pass.cpp covers byte-aligned _BitInt(N) widths from 8 up
to __BITINT_MAXWIDTH__: builtins for sizeof <= 16, the new generic loop
for sizeof > 16. The size-1 case includes _BitInt(7) (identity, despite
the padding bit) so the asymmetry with _BitInt(13) is pinned. The wide
case has a ramp pattern with a distinct byte at every position, which
surfaces any off-by-one in the loop indexing, plus low-byte and high-byte
spot checks. Wide-width tests gate on __BITINT_MAXWIDTH__ so non-x86 /
non-RISC-V64 targets compile a smaller subset.

byteswap.bitint.verify.cpp pins the static_assert added by the previous
commit. Covers _BitInt(13), _BitInt(17), _BitInt(33), _BitInt(65) on all
targets, plus _BitInt(129) and _BitInt(255) where the platform supports
them. The expected-error-re pattern matches both Clang's "static
assertion" and GCC's "static_assert" wordings.

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 .../std/numerics/bit/byteswap.bitint.pass.cpp | 131 ++++++++++++++++++
 .../numerics/bit/byteswap.bitint.verify.cpp   |  78 +++++++++++
 2 files changed, 209 insertions(+)
 create mode 100644 libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp
 create mode 100644 libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp

diff --git a/libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp b/libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp
new file mode 100644
index 0000000000000..ce432c1432c15
--- /dev/null
+++ b/libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp
@@ -0,0 +1,131 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20
+
+// <bit>
+
+// std::byteswap for _BitInt(N).
+//
+// Byte-aligned widths (N % CHAR_BIT == 0) work via the existing builtins
+// for sizeof <= 16 and via the new generic loop for sizeof > 16. Non-byte-
+// aligned widths are rejected by static_assert; that case is covered in
+// byteswap.bitint.verify.cpp.
+
+#include <bit>
+#include <cassert>
+#include <cstdint>
+
+#include "test_macros.h"
+
+#if TEST_HAS_EXTENSION(bit_int)
+
+template <class T>
+constexpr void test_roundtrip(T v) {
+  assert(std::byteswap(std::byteswap(v)) == v);
+  ASSERT_SAME_TYPE(decltype(std::byteswap(v)), T);
+  ASSERT_NOEXCEPT(std::byteswap(v));
+}
+
+constexpr bool test() {
+  // sizeof == 1: identity. The size-1 branch returns the input unchanged
+  // and the padding-bit static_assert is bypassed -- no bytes move and no
+  // padding gets shuffled into significant positions, so non-byte-aligned
+  // widths up to CHAR_BIT (e.g. _BitInt(7)) are also identity.
+  assert(std::byteswap(static_cast<unsigned _BitInt(8)>(0xAB)) == static_cast<unsigned _BitInt(8)>(0xAB));
+  test_roundtrip<unsigned _BitInt(8)>(0xAB);
+  test_roundtrip<signed _BitInt(8)>(0x12);
+  // _BitInt(7) signed has a padding bit but stays identity at sizeof == 1.
+  assert(std::byteswap(static_cast<signed _BitInt(7)>(42)) == static_cast<signed _BitInt(7)>(42));
+  assert(std::byteswap(static_cast<unsigned _BitInt(7)>(42)) == static_cast<unsigned _BitInt(7)>(42));
+
+  // sizeof == 2: __builtin_bswap16
+  assert(std::byteswap(static_cast<unsigned _BitInt(16)>(0xCDEF)) == static_cast<unsigned _BitInt(16)>(0xEFCD));
+  test_roundtrip<unsigned _BitInt(16)>(0xCDEF);
+  test_roundtrip<signed _BitInt(16)>(0x1234);
+
+  // sizeof == 4: __builtin_bswap32
+  assert(std::byteswap(static_cast<unsigned _BitInt(32)>(0x01234567U)) ==
+         static_cast<unsigned _BitInt(32)>(0x67452301U));
+  test_roundtrip<unsigned _BitInt(32)>(0x01234567U);
+  test_roundtrip<signed _BitInt(32)>(0x01234567);
+
+  // sizeof == 8: __builtin_bswap64
+  assert(std::byteswap(static_cast<unsigned _BitInt(64)>(0x0123456789ABCDEFULL)) ==
+         static_cast<unsigned _BitInt(64)>(0xEFCDAB8967452301ULL));
+  test_roundtrip<unsigned _BitInt(64)>(0x0123456789ABCDEFULL);
+  test_roundtrip<signed _BitInt(64)>(0x0123456789ABCDEFLL);
+
+#  if __BITINT_MAXWIDTH__ >= 128
+  // sizeof == 16: __builtin_bswap128 (or 2x bswap64 fallback). Same path
+  // as the existing __int128_t / __uint128_t coverage in byteswap.pass.cpp.
+  unsigned _BitInt(128) v128 =
+      (static_cast<unsigned _BitInt(128)>(0x0123456789ABCDEFULL) << 64) |
+      static_cast<unsigned _BitInt(128)>(0x13579BDF02468ACEULL);
+  test_roundtrip<unsigned _BitInt(128)>(v128);
+  test_roundtrip<signed _BitInt(128)>(static_cast<signed _BitInt(128)>(v128));
+#  endif
+
+#  if __BITINT_MAXWIDTH__ >= 256
+  // sizeof == 32: hits the new generic loop fallback.
+  unsigned _BitInt(256) v256 =
+      (static_cast<unsigned _BitInt(256)>(0xDEADBEEFCAFEBABEULL) << 128) |
+      (static_cast<unsigned _BitInt(256)>(0x1234567890ABCDEFULL) << 64) |
+      static_cast<unsigned _BitInt(256)>(0xFEDCBA9876543210ULL);
+  test_roundtrip<unsigned _BitInt(256)>(v256);
+  test_roundtrip<signed _BitInt(256)>(static_cast<signed _BitInt(256)>(v256));
+
+  // Spot check for the wide loop: low byte of input must end up as the
+  // high byte of the output, and high byte of input as the low byte.
+  unsigned _BitInt(256) lo_only = 0xAB;
+  auto lo_swapped               = std::byteswap(lo_only);
+  assert(static_cast<unsigned char>(lo_swapped >> ((sizeof(lo_swapped) - 1) * CHAR_BIT)) == 0xAB);
+  unsigned _BitInt(256) hi_only =
+      static_cast<unsigned _BitInt(256)>(0xCD) << ((sizeof(unsigned _BitInt(256)) - 1) * CHAR_BIT);
+  auto hi_swapped = std::byteswap(hi_only);
+  assert(static_cast<unsigned char>(hi_swapped) == 0xCD);
+
+  // Mid-value test: distinct byte at every position so an off-by-one in
+  // the loop indexing surfaces directly. Build 0x00010203...1F at bytes
+  // 0..31 then verify byteswap reverses the byte sequence.
+  unsigned _BitInt(256) ramp = 0;
+  for (int __i = 0; __i < 32; ++__i)
+    ramp |= static_cast<unsigned _BitInt(256)>(__i) << (__i * CHAR_BIT);
+  auto ramp_swapped = std::byteswap(ramp);
+  for (int __i = 0; __i < 32; ++__i)
+    assert(static_cast<unsigned char>(ramp_swapped >> ((31 - __i) * CHAR_BIT)) == __i);
+#  endif
+
+#  if __BITINT_MAXWIDTH__ >= 1024
+  // Larger width still in the generic loop.
+  unsigned _BitInt(1024) v1024 = static_cast<unsigned _BitInt(1024)>(0xAB) << ((128 - 1) * CHAR_BIT);
+  test_roundtrip<unsigned _BitInt(1024)>(v1024);
+#  endif
+
+#  if __BITINT_MAXWIDTH__ >= 4096
+  // Largest width tested. Picked to cover the upper end of what the
+  // dev-branch experiments exercised; values larger than this take a
+  // long time to constexpr-evaluate without adding much coverage.
+  unsigned _BitInt(4096) v4096 = static_cast<unsigned _BitInt(4096)>(0xAB) << ((512 - 1) * CHAR_BIT);
+  test_roundtrip<unsigned _BitInt(4096)>(v4096);
+#  endif
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+  return 0;
+}
+
+#else
+
+int main(int, char**) { return 0; }
+
+#endif
diff --git a/libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp b/libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp
new file mode 100644
index 0000000000000..7ff60a9c94463
--- /dev/null
+++ b/libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp
@@ -0,0 +1,78 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20
+
+// <bit>
+
+// std::byteswap rejects _BitInt(N) where N is not a multiple of CHAR_BIT.
+//
+// The byte-level builtins (and the generic loop fallback) treat the
+// storage representation as the value, so for a type with padding bits
+// they would shuffle padding into significant positions and produce a
+// value whose semantic meaning is unspecified. The static_assert added
+// in [libc++] Reject byteswap of types with padding bits pins the
+// rejection so the diagnostic does not regress silently into a wrong
+// value.
+
+#include <bit>
+
+#include "test_macros.h"
+
+#if TEST_HAS_EXTENSION(bit_int)
+
+void f_unsigned_13() {
+  unsigned _BitInt(13) v = 0;
+  // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+  (void)std::byteswap(v);
+}
+
+void f_signed_13() {
+  signed _BitInt(13) v = 0;
+  // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+  (void)std::byteswap(v);
+}
+
+void f_unsigned_17() {
+  unsigned _BitInt(17) v = 0;
+  // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+  (void)std::byteswap(v);
+}
+
+void f_signed_33() {
+  signed _BitInt(33) v = 0;
+  // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+  (void)std::byteswap(v);
+}
+
+void f_unsigned_65() {
+  unsigned _BitInt(65) v = 0;
+  // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+  (void)std::byteswap(v);
+}
+
+#  if __BITINT_MAXWIDTH__ >= 129
+// _BitInt(129) is wider than __int128 and only available where
+// __BITINT_MAXWIDTH__ supports it (x86 / RISC-V 64). The wide-type
+// generic loop also relies on the rejection; cover it here.
+void f_unsigned_129() {
+  unsigned _BitInt(129) v = 0;
+  // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+  (void)std::byteswap(v);
+}
+
+void f_signed_255() {
+  signed _BitInt(255) v = 0;
+  // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+  (void)std::byteswap(v);
+}
+#  endif
+
+#else
+// expected-no-diagnostics
+#endif



More information about the libcxx-commits mailing list