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

via libcxx-commits libcxx-commits at lists.llvm.org
Fri May 29 07:04:12 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-libcxx

Author: Xavier Roche (xroche)

<details>
<summary>Changes</summary>

`__is_signed_integer_v` and `__is_unsigned_integer_v` previously admitted cv-qualified types, contradicting `[basic.fundamental]/p1-2`. The fix adds `__is_unqualified_v<_Tp>` to both predicates.

Reported by @<!-- -->jwakely on [#<!-- -->185027](https://github.com/llvm/llvm-project/pull/185027#issuecomment-4574289895). Tests cover all templates that consume the trait (per @<!-- -->frederick-vs-ja's enumeration in `<utility>`, `<bit>`, `<numeric>`, `<mdspan>`), plus a `make_format_args` regression test pinning down the resulting `volatile int` rejection in `<format>`.

Assisted-by: Claude (Anthropic)

---

Patch is 24.67 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/200377.diff


13 Files Affected:

- (modified) libcxx/include/__type_traits/integer_traits.h (+9-7) 
- (modified) libcxx/test/libcxx/concepts/concepts.arithmetic/__libcpp_signed_integer.compile.pass.cpp (+32) 
- (modified) libcxx/test/libcxx/concepts/concepts.arithmetic/__libcpp_unsigned_integer.compile.pass.cpp (+32) 
- (modified) libcxx/test/std/containers/views/mdspan/extents/index_type.verify.cpp (+8) 
- (added) libcxx/test/std/numerics/bit/bit.cv_qualified.compile.pass.cpp (+95) 
- (modified) libcxx/test/std/numerics/bit/bit.pow.two/bit_ceil.verify.cpp (+7) 
- (modified) libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_add.compile.pass.cpp (+22) 
- (modified) libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_cast.compile.pass.cpp (+27) 
- (modified) libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_div.compile.pass.cpp (+22) 
- (modified) libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_mul.compile.pass.cpp (+22) 
- (modified) libcxx/test/std/numerics/numeric.ops/numeric.ops.sat/saturating_sub.compile.pass.cpp (+22) 
- (added) libcxx/test/std/utilities/format/format.arguments/format.arg.store/make_format_args.cv.verify.cpp (+53) 
- (modified) libcxx/test/std/utilities/utility/utility.intcmp/intcmp.verify.cpp (+20) 


``````````diff
diff --git a/libcxx/include/__type_traits/integer_traits.h b/libcxx/include/__type_traits/integer_traits.h
index d7ac89be9c2a7..a0fbcd5c3ecd7 100644
--- a/libcxx/include/__type_traits/integer_traits.h
+++ b/libcxx/include/__type_traits/integer_traits.h
@@ -13,6 +13,7 @@
 #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>
 
 #if !defined(_LIBCPP_HAS_NO_PRAGMA_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_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_integral<_Tp>::value && is_unsigned<_Tp>::value && !__is_character_v<_Tp> && !is_same<_Tp, bool>::value &&
+    __is_unqualified_v<_Tp>;
 
 #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
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..bdb88168996d9
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++20
+
+// <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..107bf8cc4728a 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,25 @@ 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); };
+
+// Unqualified signed/unsigned integers pass; bool stays rejected.
+static_assert(can_saturating_add<int>);
+static_assert(can_saturating_add<unsigned int>);
+static_assert(can_saturating_add<long long>);
+static_assert(!can_saturating_add<bool>);
+static_assert(!can_saturating_add<char>);
+
+// cv-qualified versions are rejected.
+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..3db3d841fdfaa 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,30 @@ 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); };
+
+// Unqualified signed/unsigned integers pass on both R and T sides.
+static_assert(can_cast_R<int>);
+static_assert(can_cast_R<unsigned int>);
+static_assert(can_cast_T<long long>);
+static_assert(!can_cast_R<bool>);
+static_assert(!can_cast_R<char>);
+static_assert(!can_cast_T<bool>);
+
+// cv-qualified versions are rejected on either arm.
+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..8c6b8dc08c65c 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,25 @@ 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); };
+
+// Unqualified signed/unsigned integers pass; bool stays rejected.
+static_assert(can_saturating_div<int>);
+static_assert(can_saturating_div<unsigned int>);
+static_assert(can_saturating_div<long long>);
+static_assert(!can_saturating_div<bool>);
+static_assert(!can_saturating_div<char>);
+
+// cv-qualified versions are rejected.
+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..b4a5cbea8f48e 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,25 @@ 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); };
+
+// Unqualified signed/unsigned integers pass; bool stays rejected.
+static_assert(can_saturating_mul<int>);
+static_assert(can_saturating_mul<unsigned int>);
+static_assert(can_saturating_mul<long long>);
+static_assert(!can_saturating_mul<bool>);
+static_assert(!can_saturating_mul<char>);
+
+// cv-qualified versions are rejected.
+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..7a6e7497aff47 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,25 @@ 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); };
+
+// Unqualified signed/unsigned integers pass; bool stays rejected.
+static_assert(can_saturating_sub<int>);
+static_assert(can_saturating_sub<unsigned int>);
+static_assert(can_saturating_sub<long long>);
+static_assert(!can_saturating_sub<bool>);
+static_assert(!can_saturating_sub<char>);
+
+// cv-qualified versions are rejected.
+st...
[truncated]

``````````

</details>


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


More information about the libcxx-commits mailing list