[libcxx-commits] [libcxx] [libc++] Reject cv-qualified types in __is_signed/unsigned_integer_v (PR #200377)

Xavier Roche via libcxx-commits libcxx-commits at lists.llvm.org
Fri May 29 06:00:37 PDT 2026


https://github.com/xroche updated https://github.com/llvm/llvm-project/pull/200377

>From a5ae4e1131e3139de2e039b29784c0c8cdb721f8 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Fri, 29 May 2026 13:42:52 +0200
Subject: [PATCH 1/3] [libc++] Reject cv-qualified types in
 __is_signed/unsigned_integer_v

Address jwakely's review feedback on PR #185027: the standard's
definition of /signed integer type/ and /unsigned integer type/ in
[basic.fundamental]/p1-2 is a specific list of unqualified types
(plus extended integer types). cv-qualified versions are distinct
types per [basic.type.qualifier] and so are NOT signed/unsigned
integer types.

The previous implementation wrongly admitted `const int`, `volatile
int`, `const volatile int`, `const char`, `const bool`, and similar
cv-qualified versions. The fix adds `is_same<__remove_cv_t<T>, T>`
to both traits.

Tests cover the three meaningful cv flavors (const, volatile, const
volatile), references, character types, bool, and _BitInt(N)
extended integer types.

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 libcxx/include/__type_traits/integer_traits.h | 16 ++++++----
 .../__libcpp_signed_integer.compile.pass.cpp  | 32 +++++++++++++++++++
 ...__libcpp_unsigned_integer.compile.pass.cpp | 32 +++++++++++++++++++
 3 files changed, 73 insertions(+), 7 deletions(-)

diff --git a/libcxx/include/__type_traits/integer_traits.h b/libcxx/include/__type_traits/integer_traits.h
index d7ac89be9c2a7..e0a9e5bcc98eb 100644
--- a/libcxx/include/__type_traits/integer_traits.h
+++ b/libcxx/include/__type_traits/integer_traits.h
@@ -14,6 +14,7 @@
 #include <__type_traits/is_same.h>
 #include <__type_traits/is_signed.h>
 #include <__type_traits/is_unsigned.h>
+#include <__type_traits/remove_cv.h>
 
 #if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
 #  pragma GCC system_header
@@ -21,11 +22,10 @@
 
 _LIBCPP_BEGIN_NAMESPACE_STD
 
-// These traits determine whether a type is a /signed integer type/ or
-// /unsigned integer type/ per [basic.fundamental]/p1-2.
-//
-// Character types (char, wchar_t, char8_t, char16_t, char32_t) and bool
-// are integral but are NOT signed/unsigned integer types.
+// /signed integer type/ and /unsigned integer type/ per [basic.fundamental]
+// /p1-2: specific unqualified types plus extended integer types. bool and
+// character types are integral but excluded. cv-qualified versions are
+// distinct types ([basic.type.qualifier]) and so excluded.
 
 template <class _Tp>
 inline const bool __is_character_v = false;
@@ -44,11 +44,13 @@ inline const bool __is_character_v<char32_t> = true;
 
 template <class _Tp>
 inline const bool __is_signed_integer_v =
-    is_integral<_Tp>::value && is_signed<_Tp>::value && !__is_character_v<_Tp> && !is_same<_Tp, bool>::value;
+    is_integral<_Tp>::value && is_signed<_Tp>::value && !__is_character_v<_Tp> && !is_same<_Tp, bool>::value &&
+    is_same<__remove_cv_t<_Tp>, _Tp>::value;
 
 template <class _Tp>
 inline const bool __is_unsigned_integer_v =
-    is_integral<_Tp>::value && is_unsigned<_Tp>::value && !__is_character_v<_Tp> && !is_same<_Tp, bool>::value;
+    is_integral<_Tp>::value && is_unsigned<_Tp>::value && !__is_character_v<_Tp> && !is_same<_Tp, bool>::value &&
+    is_same<__remove_cv_t<_Tp>, _Tp>::value;
 
 #if _LIBCPP_STD_VER >= 20
 template <class _Tp>
diff --git a/libcxx/test/libcxx/concepts/concepts.arithmetic/__libcpp_signed_integer.compile.pass.cpp b/libcxx/test/libcxx/concepts/concepts.arithmetic/__libcpp_signed_integer.compile.pass.cpp
index 3fa342685770c..1f2d9685bbe5a 100644
--- a/libcxx/test/libcxx/concepts/concepts.arithmetic/__libcpp_signed_integer.compile.pass.cpp
+++ b/libcxx/test/libcxx/concepts/concepts.arithmetic/__libcpp_signed_integer.compile.pass.cpp
@@ -61,3 +61,35 @@ static_assert(!std::__signed_integer<unsigned int*>);
 static_assert(!std::__signed_integer<SomeObject>);
 static_assert(!std::__signed_integer<SomeEnum>);
 static_assert(!std::__signed_integer<SomeScopedEnum>);
+
+// cv-qualified versions are distinct types ([basic.type.qualifier]) and so
+// not signed integer types per [basic.fundamental]/p1. The three meaningful
+// flavors in C++ are const, volatile, and const volatile.
+static_assert(!std::__signed_integer<const int>);
+static_assert(!std::__signed_integer<volatile int>);
+static_assert(!std::__signed_integer<const volatile int>);
+static_assert(!std::__signed_integer<const long long>);
+static_assert(!std::__signed_integer<const signed char>);
+static_assert(!std::__signed_integer<const char>);
+static_assert(!std::__signed_integer<const bool>);
+#ifndef TEST_HAS_NO_WIDE_CHARACTERS
+static_assert(!std::__signed_integer<const wchar_t>);
+#endif
+static_assert(!std::__signed_integer<int&>);
+static_assert(!std::__signed_integer<const int&>);
+
+// Extended signed integer types per [basic.fundamental]/p3 Note 1.
+#if TEST_HAS_EXTENSION(bit_int)
+static_assert(std::__signed_integer<signed _BitInt(8)>);
+static_assert(std::__signed_integer<signed _BitInt(16)>);
+static_assert(std::__signed_integer<signed _BitInt(64)>);
+static_assert(std::__signed_integer<signed _BitInt(13)>);
+static_assert(!std::__signed_integer<unsigned _BitInt(16)>);
+static_assert(!std::__signed_integer<const signed _BitInt(16)>);
+static_assert(!std::__signed_integer<volatile signed _BitInt(64)>);
+static_assert(!std::__signed_integer<const volatile signed _BitInt(13)>);
+#  if __BITINT_MAXWIDTH__ >= 128
+static_assert(std::__signed_integer<signed _BitInt(128)>);
+static_assert(!std::__signed_integer<const signed _BitInt(128)>);
+#  endif
+#endif
diff --git a/libcxx/test/libcxx/concepts/concepts.arithmetic/__libcpp_unsigned_integer.compile.pass.cpp b/libcxx/test/libcxx/concepts/concepts.arithmetic/__libcpp_unsigned_integer.compile.pass.cpp
index ff60f32319171..3f78f170b7038 100644
--- a/libcxx/test/libcxx/concepts/concepts.arithmetic/__libcpp_unsigned_integer.compile.pass.cpp
+++ b/libcxx/test/libcxx/concepts/concepts.arithmetic/__libcpp_unsigned_integer.compile.pass.cpp
@@ -61,3 +61,35 @@ static_assert(!std::__unsigned_integer<unsigned int*>);
 static_assert(!std::__unsigned_integer<SomeObject>);
 static_assert(!std::__unsigned_integer<SomeEnum>);
 static_assert(!std::__unsigned_integer<SomeScopedEnum>);
+
+// cv-qualified versions are distinct types ([basic.type.qualifier]) and so
+// not unsigned integer types per [basic.fundamental]/p2. The three meaningful
+// flavors in C++ are const, volatile, and const volatile.
+static_assert(!std::__unsigned_integer<const unsigned int>);
+static_assert(!std::__unsigned_integer<volatile unsigned int>);
+static_assert(!std::__unsigned_integer<const volatile unsigned int>);
+static_assert(!std::__unsigned_integer<const unsigned long long>);
+static_assert(!std::__unsigned_integer<const unsigned char>);
+static_assert(!std::__unsigned_integer<const char>);
+static_assert(!std::__unsigned_integer<const bool>);
+static_assert(!std::__unsigned_integer<const char8_t>);
+static_assert(!std::__unsigned_integer<const char16_t>);
+static_assert(!std::__unsigned_integer<const char32_t>);
+static_assert(!std::__unsigned_integer<unsigned int&>);
+static_assert(!std::__unsigned_integer<const unsigned int&>);
+
+// Extended unsigned integer types per [basic.fundamental]/p3 Note 1.
+#if TEST_HAS_EXTENSION(bit_int)
+static_assert(std::__unsigned_integer<unsigned _BitInt(8)>);
+static_assert(std::__unsigned_integer<unsigned _BitInt(16)>);
+static_assert(std::__unsigned_integer<unsigned _BitInt(64)>);
+static_assert(std::__unsigned_integer<unsigned _BitInt(13)>);
+static_assert(!std::__unsigned_integer<signed _BitInt(16)>);
+static_assert(!std::__unsigned_integer<const unsigned _BitInt(16)>);
+static_assert(!std::__unsigned_integer<volatile unsigned _BitInt(64)>);
+static_assert(!std::__unsigned_integer<const volatile unsigned _BitInt(13)>);
+#  if __BITINT_MAXWIDTH__ >= 128
+static_assert(std::__unsigned_integer<unsigned _BitInt(128)>);
+static_assert(!std::__unsigned_integer<const unsigned _BitInt(128)>);
+#  endif
+#endif

>From 50b9185ffe793ceaba9c5ede84aa3aa4182bd21b Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Fri, 29 May 2026 14:45:39 +0200
Subject: [PATCH 2/3] [libc++] Use __is_unqualified_v and add user-facing cv
 regression tests

Address Nikolas/jwakely review feedback on PR #200377:

1. Swap `is_same<__remove_cv_t<_Tp>, _Tp>::value` to `__is_unqualified_v<_Tp>`,
   the established libc++ idiom (uses compiler intrinsics
   `__is_same`/`__remove_cvref`, cheaper than library-trait instantiation).

2. Add user-facing regression tests covering frederick-vs-ja's enumerated
   templates that consume the trait:

   Mandates (verify.cpp with expected-error):
   - `<utility>`: cmp_equal, cmp_not_equal, cmp_less, cmp_less_equal,
     cmp_greater, cmp_greater_equal, in_range (covers jwakely's specific
     `std::in_range<const int>(0)` libstdc++-parity bug).
   - `<mdspan>`: `extents<const int, ...>` and cv variants.

   Constraints (compile.pass.cpp with concept-based detection):
   - `<bit>` (new bit.cv_qualified.compile.pass.cpp): has_single_bit,
     bit_ceil, bit_floor, bit_width, rotl, rotr, countl_zero, countl_one,
     countr_zero, countr_one, popcount.
   - `<numeric>`: saturating_add, saturating_sub, saturating_mul,
     saturating_div, saturating_cast (both R and T arms).

   `byteswap` is NOT in this list: its Constraints clause is `T models
   integral`, which admits cv-qualified types.

Note: `<format>` is also indirectly affected (arg-store dispatch consumes
`__signed_integer`/`__unsigned_integer`), but `__create_format_arg` strips
`const` via `remove_const_t` so non-volatile cv paths remain working.
`format("{}", volatile_int)` is a deliberate spec-correctness behavior
change matching libstdc++.

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 libcxx/include/__type_traits/integer_traits.h |  6 +-
 .../mdspan/extents/index_type.verify.cpp      |  8 ++
 .../bit/bit.cv_qualified.compile.pass.cpp     | 95 +++++++++++++++++++
 .../bit/bit.pow.two/bit_ceil.verify.cpp       |  7 ++
 .../saturating_add.compile.pass.cpp           | 13 +++
 .../saturating_cast.compile.pass.cpp          | 17 ++++
 .../saturating_div.compile.pass.cpp           | 13 +++
 .../saturating_mul.compile.pass.cpp           | 13 +++
 .../saturating_sub.compile.pass.cpp           | 13 +++
 .../utility/utility.intcmp/intcmp.verify.cpp  | 20 ++++
 10 files changed, 202 insertions(+), 3 deletions(-)
 create mode 100644 libcxx/test/std/numerics/bit/bit.cv_qualified.compile.pass.cpp

diff --git a/libcxx/include/__type_traits/integer_traits.h b/libcxx/include/__type_traits/integer_traits.h
index e0a9e5bcc98eb..a0fbcd5c3ecd7 100644
--- a/libcxx/include/__type_traits/integer_traits.h
+++ b/libcxx/include/__type_traits/integer_traits.h
@@ -13,8 +13,8 @@
 #include <__type_traits/is_integral.h>
 #include <__type_traits/is_same.h>
 #include <__type_traits/is_signed.h>
+#include <__type_traits/is_unqualified.h>
 #include <__type_traits/is_unsigned.h>
-#include <__type_traits/remove_cv.h>
 
 #if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
 #  pragma GCC system_header
@@ -45,12 +45,12 @@ inline const bool __is_character_v<char32_t> = true;
 template <class _Tp>
 inline const bool __is_signed_integer_v =
     is_integral<_Tp>::value && is_signed<_Tp>::value && !__is_character_v<_Tp> && !is_same<_Tp, bool>::value &&
-    is_same<__remove_cv_t<_Tp>, _Tp>::value;
+    __is_unqualified_v<_Tp>;
 
 template <class _Tp>
 inline const bool __is_unsigned_integer_v =
     is_integral<_Tp>::value && is_unsigned<_Tp>::value && !__is_character_v<_Tp> && !is_same<_Tp, bool>::value &&
-    is_same<__remove_cv_t<_Tp>, _Tp>::value;
+    __is_unqualified_v<_Tp>;
 
 #if _LIBCPP_STD_VER >= 20
 template <class _Tp>
diff --git a/libcxx/test/std/containers/views/mdspan/extents/index_type.verify.cpp b/libcxx/test/std/containers/views/mdspan/extents/index_type.verify.cpp
index ba6941a1ab4c1..36922c0dac2ba 100644
--- a/libcxx/test/std/containers/views/mdspan/extents/index_type.verify.cpp
+++ b/libcxx/test/std/containers/views/mdspan/extents/index_type.verify.cpp
@@ -35,6 +35,14 @@ void invalid_index_types() {
   // expected-error@*:* {{static assertion failed: extents::index_type must be a signed or unsigned integer type}}
   [[maybe_unused]] std::extents<wchar_t, L'*'> ewc;
 #endif
+  // cv-qualified types are not signed/unsigned integer types per
+  // [basic.fundamental]/p1-2.
+  // expected-error@*:* {{static assertion failed: extents::index_type must be a signed or unsigned integer type}}
+  [[maybe_unused]] std::extents<const int, 1> eci;
+  // expected-error@*:* {{static assertion failed: extents::index_type must be a signed or unsigned integer type}}
+  [[maybe_unused]] std::extents<volatile int, 1> evi;
+  // expected-error@*:* {{static assertion failed: extents::index_type must be a signed or unsigned integer type}}
+  [[maybe_unused]] std::extents<const volatile unsigned, 1> ecvu;
 }
 
 void invalid_extent_values() {
diff --git a/libcxx/test/std/numerics/bit/bit.cv_qualified.compile.pass.cpp b/libcxx/test/std/numerics/bit/bit.cv_qualified.compile.pass.cpp
new file mode 100644
index 0000000000000..7353a3a764f6d
--- /dev/null
+++ b/libcxx/test/std/numerics/bit/bit.cv_qualified.compile.pass.cpp
@@ -0,0 +1,95 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+
+// <bit>
+
+// Regression test for [bit] templates constrained on __unsigned_integer:
+// cv-qualified types are not unsigned integer types per [basic.fundamental]
+// /p2 and must be rejected by the SFINAE constraint. Explicit template args
+// bypass the by-value deduction strip, so the rejection has to live in the
+// constraint, not just at call site.
+//
+// std::byteswap is NOT in this list: its Constraints clause is `T models
+// integral`, which admits cv-qualified types.
+
+#include <bit>
+
+template <class T>
+concept _has_bit_ceil = requires { std::bit_ceil<T>(T{}); };
+template <class T>
+concept _has_bit_floor = requires { std::bit_floor<T>(T{}); };
+template <class T>
+concept _has_bit_width = requires { std::bit_width<T>(T{}); };
+template <class T>
+concept _has_has_single_bit = requires { std::has_single_bit<T>(T{}); };
+template <class T>
+concept _has_rotl = requires { std::rotl<T>(T{}, 0); };
+template <class T>
+concept _has_rotr = requires { std::rotr<T>(T{}, 0); };
+template <class T>
+concept _has_countl_zero = requires { std::countl_zero<T>(T{}); };
+template <class T>
+concept _has_countl_one = requires { std::countl_one<T>(T{}); };
+template <class T>
+concept _has_countr_zero = requires { std::countr_zero<T>(T{}); };
+template <class T>
+concept _has_countr_one = requires { std::countr_one<T>(T{}); };
+template <class T>
+concept _has_popcount = requires { std::popcount<T>(T{}); };
+
+template <class T>
+constexpr void check_admitted() {
+  static_assert(_has_bit_ceil<T>);
+  static_assert(_has_bit_floor<T>);
+  static_assert(_has_bit_width<T>);
+  static_assert(_has_has_single_bit<T>);
+  static_assert(_has_rotl<T>);
+  static_assert(_has_rotr<T>);
+  static_assert(_has_countl_zero<T>);
+  static_assert(_has_countl_one<T>);
+  static_assert(_has_countr_zero<T>);
+  static_assert(_has_countr_one<T>);
+  static_assert(_has_popcount<T>);
+}
+
+template <class T>
+constexpr void check_rejected() {
+  static_assert(!_has_bit_ceil<T>);
+  static_assert(!_has_bit_floor<T>);
+  static_assert(!_has_bit_width<T>);
+  static_assert(!_has_has_single_bit<T>);
+  static_assert(!_has_rotl<T>);
+  static_assert(!_has_rotr<T>);
+  static_assert(!_has_countl_zero<T>);
+  static_assert(!_has_countl_one<T>);
+  static_assert(!_has_countr_zero<T>);
+  static_assert(!_has_countr_one<T>);
+  static_assert(!_has_popcount<T>);
+}
+
+// Unqualified unsigned integer types pass.
+template void check_admitted<unsigned int>();
+template void check_admitted<unsigned long>();
+template void check_admitted<unsigned long long>();
+
+// cv-qualified versions of unsigned integer types are rejected.
+template void check_rejected<const unsigned int>();
+template void check_rejected<volatile unsigned int>();
+template void check_rejected<const volatile unsigned int>();
+template void check_rejected<const unsigned long>();
+template void check_rejected<const unsigned long long>();
+
+// Signed and character types stay rejected.
+template void check_rejected<int>();
+template void check_rejected<const int>();
+template void check_rejected<bool>();
+template void check_rejected<const bool>();
+template void check_rejected<char>();
+template void check_rejected<const char>();
diff --git a/libcxx/test/std/numerics/bit/bit.pow.two/bit_ceil.verify.cpp b/libcxx/test/std/numerics/bit/bit.pow.two/bit_ceil.verify.cpp
index d37de690a48db..10abd5ba4a00e 100644
--- a/libcxx/test/std/numerics/bit/bit.pow.two/bit_ceil.verify.cpp
+++ b/libcxx/test/std/numerics/bit/bit.pow.two/bit_ceil.verify.cpp
@@ -47,5 +47,12 @@ int main(int, char**)
     static_assert(toobig<std::uintmax_t>(), ""); // expected-error {{static assertion expression is not an integral constant expression}}
     static_assert(toobig<std::uintptr_t>(), ""); // expected-error {{static assertion expression is not an integral constant expression}}
 
+    // cv-qualified versions are not unsigned integer types per
+    // [basic.fundamental]/p2. Explicit template args bypass by-value
+    // deduction strip, so the constraint must reject these.
+    std::bit_ceil<const unsigned int>(0u);            // expected-error {{no matching function for call to 'bit_ceil'}}
+    std::bit_ceil<volatile unsigned int>(0u);         // expected-error {{no matching function for call to 'bit_ceil'}}
+    std::bit_ceil<const volatile unsigned long>(0ul); // expected-error {{no matching function for call to 'bit_ceil'}}
+
     return 0;
 }
diff --git a/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_add.compile.pass.cpp b/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_add.compile.pass.cpp
index 03c7dc724acfb..988c37d074a3b 100644
--- a/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_add.compile.pass.cpp
+++ b/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_add.compile.pass.cpp
@@ -75,3 +75,16 @@ constexpr void test() {
   test_constraint_fail<double>();
   test_constraint_fail<long double>();
 }
+
+// cv-qualified versions are not signed/unsigned integer types per
+// [basic.fundamental]/p1-2. Explicit template args bypass by-value
+// deduction strip, so the constraint must reject these.
+template <class T>
+concept _can_saturating_add = requires(int x) { std::saturating_add<T>(x, x); };
+static_assert(!_can_saturating_add<const int>);
+static_assert(!_can_saturating_add<volatile int>);
+static_assert(!_can_saturating_add<const volatile int>);
+static_assert(!_can_saturating_add<const unsigned int>);
+static_assert(!_can_saturating_add<const long long>);
+static_assert(!_can_saturating_add<const bool>);
+static_assert(!_can_saturating_add<const char>);
diff --git a/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_cast.compile.pass.cpp b/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_cast.compile.pass.cpp
index b8d015811798b..f7ccfe10bc702 100644
--- a/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_cast.compile.pass.cpp
+++ b/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_cast.compile.pass.cpp
@@ -77,3 +77,20 @@ constexpr void test() {
   test_constraint_fail<double>();
   test_constraint_fail<long double>();
 }
+
+// cv-qualified versions are not signed/unsigned integer types per
+// [basic.fundamental]/p1-2. saturating_cast<R>(T) constrains BOTH R and T
+// on __signed_or_unsigned_integer; either being cv-qualified must reject.
+template <class R>
+concept _can_cast_R = requires(int x) { std::saturating_cast<R>(x); };
+template <class T>
+concept _can_cast_T = requires(T x) { std::saturating_cast<int, T>(x); };
+static_assert(!_can_cast_R<const int>);
+static_assert(!_can_cast_R<volatile int>);
+static_assert(!_can_cast_R<const volatile int>);
+static_assert(!_can_cast_R<const unsigned int>);
+static_assert(!_can_cast_R<const bool>);
+static_assert(!_can_cast_R<const char>);
+static_assert(!_can_cast_T<const int>);
+static_assert(!_can_cast_T<volatile int>);
+static_assert(!_can_cast_T<const volatile int>);
diff --git a/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_div.compile.pass.cpp b/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_div.compile.pass.cpp
index f644b11db05cd..ebefabd41f4cf 100644
--- a/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_div.compile.pass.cpp
+++ b/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_div.compile.pass.cpp
@@ -106,3 +106,16 @@ static_assert(!CanDivByZero<0ULL>);
 #ifndef TEST_HAS_NO_INT128
 static_assert(!CanDivByZero<static_cast<__uint128_t>(0)>);
 #endif
+
+// cv-qualified versions are not signed/unsigned integer types per
+// [basic.fundamental]/p1-2. Explicit template args bypass by-value
+// deduction strip, so the constraint must reject these.
+template <class T>
+concept _can_saturating_div = requires(int x) { std::saturating_div<T>(x, x); };
+static_assert(!_can_saturating_div<const int>);
+static_assert(!_can_saturating_div<volatile int>);
+static_assert(!_can_saturating_div<const volatile int>);
+static_assert(!_can_saturating_div<const unsigned int>);
+static_assert(!_can_saturating_div<const long long>);
+static_assert(!_can_saturating_div<const bool>);
+static_assert(!_can_saturating_div<const char>);
diff --git a/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_mul.compile.pass.cpp b/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_mul.compile.pass.cpp
index 418c479cd22c6..aa214006a8402 100644
--- a/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_mul.compile.pass.cpp
+++ b/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_mul.compile.pass.cpp
@@ -75,3 +75,16 @@ constexpr void test() {
   test_constraint_fail<double>();
   test_constraint_fail<long double>();
 }
+
+// cv-qualified versions are not signed/unsigned integer types per
+// [basic.fundamental]/p1-2. Explicit template args bypass by-value
+// deduction strip, so the constraint must reject these.
+template <class T>
+concept _can_saturating_mul = requires(int x) { std::saturating_mul<T>(x, x); };
+static_assert(!_can_saturating_mul<const int>);
+static_assert(!_can_saturating_mul<volatile int>);
+static_assert(!_can_saturating_mul<const volatile int>);
+static_assert(!_can_saturating_mul<const unsigned int>);
+static_assert(!_can_saturating_mul<const long long>);
+static_assert(!_can_saturating_mul<const bool>);
+static_assert(!_can_saturating_mul<const char>);
diff --git a/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_sub.compile.pass.cpp b/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_sub.compile.pass.cpp
index 9676d29b2c32c..6bc6d7ebaa96c 100644
--- a/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_sub.compile.pass.cpp
+++ b/libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_sub.compile.pass.cpp
@@ -75,3 +75,16 @@ constexpr void test() {
   test_constraint_fail<double>();
   test_constraint_fail<long double>();
 }
+
+// cv-qualified versions are not signed/unsigned integer types per
+// [basic.fundamental]/p1-2. Explicit template args bypass by-value
+// deduction strip, so the constraint must reject these.
+template <class T>
+concept _can_saturating_sub = requires(int x) { std::saturating_sub<T>(x, x); };
+static_assert(!_can_saturating_sub<const int>);
+static_assert(!_can_saturating_sub<volatile int>);
+static_assert(!_can_saturating_sub<const volatile int>);
+static_assert(!_can_saturating_sub<const unsigned int>);
+static_assert(!_can_saturating_sub<const long long>);
+static_assert(!_can_saturating_sub<const bool>);
+static_assert(!_can_saturating_sub<const char>);
diff --git a/libcxx/test/std/utilities/utility/utility.intcmp/intcmp.verify.cpp b/libcxx/test/std/utilities/utility/utility.intcmp/intcmp.verify.cpp
index 07ea511ae30a7..babbbde6c8ab4 100644
--- a/libcxx/test/std/utilities/utility/utility.intcmp/intcmp.verify.cpp
+++ b/libcxx/test/std/utilities/utility/utility.intcmp/intcmp.verify.cpp
@@ -121,6 +121,26 @@ constexpr void test_uchars() {
   std::in_range<int>(T()); // expected-error 2 {{no matching function for call to 'in_range'}}
 }
 
+// cv-qualified types are not signed/unsigned integer types per
+// [basic.fundamental]/p1-2. Explicit template args bypass the by-value
+// deduction strip, so the constraint must reject these.
+void test_cv() {
+  std::cmp_equal<const int, const int>(0, 0);   // expected-error {{no matching function for call to 'cmp_equal'}}
+  std::cmp_not_equal<const int, int>(0, 0);     // expected-error {{no matching function for call to 'cmp_not_equal'}}
+  std::cmp_less<const int, int>(0, 0);          // expected-error {{no matching function for call to 'cmp_less'}}
+  std::cmp_less<int, volatile int>(0, 0);       // expected-error {{no matching function for call to 'cmp_less'}}
+  std::cmp_less_equal<volatile int, int>(0, 0); // expected-error {{no matching function for call to 'cmp_less_equal'}}
+  std::cmp_greater<const volatile int, const int>(
+      0, 0); // expected-error {{no matching function for call to 'cmp_greater'}}
+  std::cmp_greater_equal<const int, const int>(
+      0, 0);                             // expected-error {{no matching function for call to 'cmp_greater_equal'}}
+  std::in_range<const int>(0);           // expected-error {{no matching function for call to 'in_range'}}
+  std::in_range<volatile int>(0);        // expected-error {{no matching function for call to 'in_range'}}
+  std::in_range<const bool>(0);          // expected-error {{no matching function for call to 'in_range'}}
+  std::in_range<const char>(0);          // expected-error {{no matching function for call to 'in_range'}}
+  std::in_range<const unsigned long>(0); // expected-error {{no matching function for call to 'in_range'}}
+}
+
 int main(int, char**) {
   test<bool>();
   test<char>();

>From ed863bca75d40638722f428b5f05ee54ee82786c Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Fri, 29 May 2026 14:54:58 +0200
Subject: [PATCH 3/3] [libc++] Add cv-rejection regression test for
 make_format_args

`__create_format_arg` strips `const` via `remove_const_t` before
dispatch, so `make_format_args(const_int_lvalue)` still works
(classified as a signed integer). It does NOT strip `volatile`, so
with the cv-rejection now in `__signed_integer`/`__unsigned_integer`,
a `volatile int` lvalue hits `__arg_t::__none` and trips the existing
"the supplied type is not formattable" static_assert.

Lock in both behaviors so a future refactor does not silently change
them.

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 .../make_format_args.cv.verify.cpp            | 53 +++++++++++++++++++
 1 file changed, 53 insertions(+)
 create mode 100644 libcxx/test/std/utilities/format/format.arguments/format.arg.store/make_format_args.cv.verify.cpp

diff --git a/libcxx/test/std/utilities/format/format.arguments/format.arg.store/make_format_args.cv.verify.cpp b/libcxx/test/std/utilities/format/format.arguments/format.arg.store/make_format_args.cv.verify.cpp
new file mode 100644
index 0000000000000..7d12932349583
--- /dev/null
+++ b/libcxx/test/std/utilities/format/format.arguments/format.arg.store/make_format_args.cv.verify.cpp
@@ -0,0 +1,53 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+
+// <format>
+
+// make_format_args of a cv-qualified integer.
+//
+// `__create_format_arg` applies `remove_const_t` before dispatch via
+// `__determine_arg_t`, so a `const int` lvalue is still classified as a
+// signed integer. `volatile` is not stripped: with the no-cv guard in
+// `__signed_integer`/`__unsigned_integer`, `volatile int` no longer maps
+// to a storage slot and trips the "not formattable" static_assert chain.
+// Pin down both behaviors so a future refactor does not silently change
+// them.
+
+#include <format>
+
+#include "test_macros.h"
+
+// Three volatile lvalues below each trigger:
+//   - format_arg_store.h: "the supplied type is not formattable"
+//   - format_arg_store.h: a follow-on static_assert
+//   - constructible.h: implicitly-deleted copy constructor cascade
+// expected-error@*:* 3 {{the supplied type is not formattable}}
+// expected-error@*:* 3+ {{static assertion failed}}
+// expected-error@*:* 3+ {{implicitly-deleted copy constructor}}
+
+void f_const_int_ok() {
+  const int value = 0;
+  (void)std::make_format_args(value); // ok: const stripped before classification
+}
+
+void f_volatile_int() {
+  volatile int value = 0;
+  (void)std::make_format_args(value);
+}
+
+void f_const_volatile_int() {
+  const volatile int value = 0;
+  (void)std::make_format_args(value);
+}
+
+void f_volatile_unsigned() {
+  volatile unsigned value = 0;
+  (void)std::make_format_args(value);
+}



More information about the libcxx-commits mailing list