[libcxx-commits] [libcxx] 4463349 - [libc++] Fix constant_wrapper::operator() (#193573)

via libcxx-commits libcxx-commits at lists.llvm.org
Sat Apr 25 06:20:45 PDT 2026


Author: Hui
Date: 2026-04-25T14:20:40+01:00
New Revision: 44633491b77c3e053aebaba13c017b39a806b045

URL: https://github.com/llvm/llvm-project/commit/44633491b77c3e053aebaba13c017b39a806b045
DIFF: https://github.com/llvm/llvm-project/commit/44633491b77c3e053aebaba13c017b39a806b045.diff

LOG: [libc++] Fix constant_wrapper::operator() (#193573)

As Tomasz pointed out on mattermost, 
given

```cpp
template <class T>
struct MustBeInt {
  static_assert(std::same_as<T, int>);
};

struct Poison {
  template <class T>
  constexpr auto operator()(T) const noexcept -> MustBeInt<T> {
    return {};
  }
};

std::cw<Poison{}>(std::cw<5>);
```

This should work according to the wording
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3978r3.pdf

```cpp
template<class... Args>
static constexpr decltype(auto) operator()(Args&&... args) noexcept(see below);
```

```
- Let call-expr be constant_wrapper<INVOKE (value, remove_cvref_t<Args>::value...)>{} if all types
in remove_cvref_t<Args>... satisfy constexpr-param and constant_wrapper<INVOKE (value, remove_-
cvref_t<Args>::value...)> is a valid type, otherwise let call-expr be INVOKE (value, std::forward<Args>(args)...).
- Constraints: call-expr is a valid expression.
- Effects: Equivalent to: return call-expr;
- Remarks: The exception specification is equivalent to noexcept(call-expr).
```

so basically the spec says, there are two cases
- constexpr-param case
-  runtime case

and if constexpr-param case works, uses it, otherwise fallback to
runtime case if it is valid, otherwise substitution error.

In this case, `std::cw<5>` satisfy `constexpr-param`, and
`constant_wrapper<Poison{}(5)>` is valid , it should just work. however,
our implementation implemented the two cases in two overload

```cpp
template <class... _Args>
    requires(__constexpr_param<remove_cvref_t<_Args>> && ...)
  _LIBCPP_HIDE_FROM_ABI static constexpr auto __call(_Args&&...) noexcept
      -> constant_wrapper<std::invoke(value, remove_cvref_t<_Args>::value...)> {
  return {};
  }

 template <class... _Args>
  _LIBCPP_HIDE_FROM_ABI static constexpr auto
  __call(_Args&&... __args) noexcept(noexcept(std::invoke(value, std::forward<_Args>(__args)...)))
      -> decltype(std::invoke(value, std::forward<_Args>(__args)...)) {
    return std::invoke(value, std::forward<_Args>(__args)...);
  }
```

The logic is that, both overloads use return type SFINAE and the
constexpr-param case's concept is more constrained so it will be picked
first if constexpr-param case works.

However, in this example, the second overload's return type SFINAE
causes a hard error by the `static_assert` in the user code.

The fix is simply constrain the second overload and not using return
type SFINAE so it short circuits if constexpr-param case is valid.


As a drive by, removed the internal helper function and directly make
the public interface `operator()` two overloads so that we can apply
`[[nodiscard]]` on the constexpr-param case

Added: 
    

Modified: 
    libcxx/include/__utility/constant_wrapper.h
    libcxx/test/libcxx/utilities/const.wrap.class/nodiscard.verify.cpp
    libcxx/test/std/utilities/const.wrap.class/call.pass.cpp
    libcxx/test/std/utilities/const.wrap.class/subscript.pass.cpp

Removed: 
    


################################################################################
diff  --git a/libcxx/include/__utility/constant_wrapper.h b/libcxx/include/__utility/constant_wrapper.h
index c3e2f5e400980..6bae95fc1878a 100644
--- a/libcxx/include/__utility/constant_wrapper.h
+++ b/libcxx/include/__utility/constant_wrapper.h
@@ -12,8 +12,10 @@
 #include <__config>
 #include <__cstddef/size_t.h>
 #include <__functional/invoke.h>
+#include <__type_traits/invoke.h>
 #include <__type_traits/is_constructible.h>
 #include <__type_traits/remove_cvref.h>
+#include <__utility/declval.h>
 #include <__utility/forward.h>
 #include <__utility/integer_sequence.h>
 
@@ -276,62 +278,58 @@ struct __cw_operators {
   }
 };
 
+template <const auto& _Callable, class... _Args>
+concept __constexpr_callable = (__constexpr_param<remove_cvref_t<_Args>> && ...) && requires {
+  typename constant_wrapper<std::invoke(_Callable, remove_cvref_t<_Args>::value...)>;
+};
+
+template <const auto& _Obj, class... _Args>
+concept __constexpr_indexable = (__constexpr_param<remove_cvref_t<_Args>> && ...) && requires {
+  typename constant_wrapper<_Obj[remove_cvref_t<_Args>::value...]>;
+};
+
 template <__cw_fixed_value _Xp, class>
 struct constant_wrapper : __cw_operators {
   static constexpr const auto& value = _Xp.__data;
   using type                         = constant_wrapper;
   using value_type                   = decltype(_Xp)::__type;
 
-private:
-  template <class... _Args>
-    requires(__constexpr_param<remove_cvref_t<_Args>> && ...)
-  _LIBCPP_HIDE_FROM_ABI static constexpr auto __call(_Args&&...) noexcept
-      -> constant_wrapper<std::invoke(value, remove_cvref_t<_Args>::value...)> {
+  template <__constexpr_param _Rp>
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI constexpr auto operator=(_Rp) const noexcept
+      -> constant_wrapper<(value = _Rp::value)> {
     return {};
   }
 
-  template <class... _Args>
-  _LIBCPP_HIDE_FROM_ABI static constexpr auto
-  __call(_Args&&... __args) noexcept(noexcept(std::invoke(value, std::forward<_Args>(__args)...)))
-      -> decltype(std::invoke(value, std::forward<_Args>(__args)...)) {
-    return std::invoke(value, std::forward<_Args>(__args)...);
-  }
+  _LIBCPP_HIDE_FROM_ABI constexpr operator decltype(value)() const noexcept { return value; }
 
   template <class... _Args>
-    requires(__constexpr_param<remove_cvref_t<_Args>> && ...)
-  _LIBCPP_HIDE_FROM_ABI static constexpr auto __subscript(_Args&&...) noexcept
-      -> constant_wrapper<value[remove_cvref_t<_Args>::value...]> {
+    requires __constexpr_callable<value, _Args...>
+  [[nodiscard]]
+  _LIBCPP_HIDE_FROM_ABI static constexpr constant_wrapper<std::invoke(value, remove_cvref_t<_Args>::value...)>
+  operator()(_Args&&...) noexcept {
     return {};
   }
 
   template <class... _Args>
-  _LIBCPP_HIDE_FROM_ABI static constexpr auto
-  __subscript(_Args&&... __args) noexcept(noexcept(value[std::forward<_Args>(__args)...]))
-      -> decltype(value[std::forward<_Args>(__args)...]) {
-    return value[std::forward<_Args>(__args)...];
-  }
-
-public:
-  template <__constexpr_param _Rp>
-  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI constexpr auto operator=(_Rp) const noexcept
-      -> constant_wrapper<(value = _Rp::value)> {
-    return {};
+    requires(!__constexpr_callable<value, _Args...> && is_invocable_v<const value_type&, _Args && ...>)
+  _LIBCPP_HIDE_FROM_ABI static constexpr decltype(auto)
+  operator()(_Args&&... __args) noexcept(noexcept(std::invoke(value, std::forward<_Args>(__args)...))) {
+    return std::invoke(value, std::forward<_Args>(__args)...);
   }
 
-  _LIBCPP_HIDE_FROM_ABI constexpr operator decltype(value)() const noexcept { return value; }
-
   template <class... _Args>
-  _LIBCPP_HIDE_FROM_ABI static constexpr auto
-  operator()(_Args&&... __args) noexcept(noexcept(constant_wrapper::__call(std::forward<_Args>(__args)...)))
-      -> decltype(constant_wrapper::__call(std::forward<_Args>(__args)...)) {
-    return __call(std::forward<_Args>(__args)...);
+    requires __constexpr_indexable<value, _Args...>
+  [[nodiscard]]
+  _LIBCPP_HIDE_FROM_ABI static constexpr constant_wrapper<value[remove_cvref_t<_Args>::value...]>
+  operator[](_Args&&...) noexcept {
+    return {};
   }
 
   template <class... _Args>
-  _LIBCPP_HIDE_FROM_ABI static constexpr auto
-  operator[](_Args&&... __args) noexcept(noexcept(constant_wrapper::__subscript(std::forward<_Args>(__args)...)))
-      -> decltype(constant_wrapper::__subscript(std::forward<_Args>(__args)...)) {
-    return __subscript(std::forward<_Args>(__args)...);
+    requires(!__constexpr_indexable<value, _Args...> && requires { value[std::declval<_Args>()...]; })
+  _LIBCPP_HIDE_FROM_ABI static constexpr decltype(auto)
+  operator[](_Args&&... __args) noexcept(noexcept(value[std::forward<_Args>(__args)...])) {
+    return value[std::forward<_Args>(__args)...];
   }
 };
 

diff  --git a/libcxx/test/libcxx/utilities/const.wrap.class/nodiscard.verify.cpp b/libcxx/test/libcxx/utilities/const.wrap.class/nodiscard.verify.cpp
index 1b8ac6ecc4cca..7e2a7e4f6e828 100644
--- a/libcxx/test/libcxx/utilities/const.wrap.class/nodiscard.verify.cpp
+++ b/libcxx/test/libcxx/utilities/const.wrap.class/nodiscard.verify.cpp
@@ -35,6 +35,9 @@ struct Ops {
   constexpr Ops operator>>=(Ops) const { return {}; }
 
   constexpr Ops operator=(auto) const { return {}; }
+
+  constexpr Ops operator()(Ops) const { return {}; }
+  constexpr Ops operator[](Ops) const { return {}; }
 };
 
 void test() {
@@ -140,4 +143,10 @@ void test() {
   std::constant_wrapper<Ops{}> a;
   // expected-warning at +1 {{ignoring return value of function declared with 'nodiscard' attribute}}
   a = std::cw<5>;
+
+  // expected-warning at +1 {{ignoring return value of function declared with 'nodiscard' attribute}}
+  std::cw<Ops{}>(std::cw<Ops{}>);
+
+  // expected-warning at +1 {{ignoring return value of function declared with 'nodiscard' attribute}}
+  std::cw<Ops{}>[std::cw<Ops{}>];
 }

diff  --git a/libcxx/test/std/utilities/const.wrap.class/call.pass.cpp b/libcxx/test/std/utilities/const.wrap.class/call.pass.cpp
index d63a92f62c6ff..dfa8813e47a8d 100644
--- a/libcxx/test/std/utilities/const.wrap.class/call.pass.cpp
+++ b/libcxx/test/std/utilities/const.wrap.class/call.pass.cpp
@@ -82,6 +82,18 @@ static_assert(std::is_nothrow_invocable_v<std::constant_wrapper<throwing_call>,
               "the call expression is still nothrow because the constexpr path is taken");
 // clang-format on
 
+template <class T>
+struct MustBeInt {
+  static_assert(std::same_as<T, int>);
+};
+
+struct Poison {
+  template <class T>
+  constexpr auto operator()(T) const noexcept -> MustBeInt<T> {
+    return {};
+  }
+};
+
 constexpr bool test() {
   {
     // with runtime param
@@ -227,6 +239,12 @@ constexpr bool test() {
     static_assert(result == 3);
   }
 
+  {
+    using T = std::constant_wrapper<Poison{}>;
+    [[maybe_unused]] std::same_as<std::constant_wrapper<MustBeInt<int>{}>> decltype(auto) result =
+        T::operator()(std::cw<5>);
+  }
+
   return true;
 }
 

diff  --git a/libcxx/test/std/utilities/const.wrap.class/subscript.pass.cpp b/libcxx/test/std/utilities/const.wrap.class/subscript.pass.cpp
index 697f31cb1df04..7efefa85fed95 100644
--- a/libcxx/test/std/utilities/const.wrap.class/subscript.pass.cpp
+++ b/libcxx/test/std/utilities/const.wrap.class/subscript.pass.cpp
@@ -85,6 +85,18 @@ static_assert(!HasNothrowSubscript<std::constant_wrapper<ThrowingSubscript{}>, i
 static_assert(HasNothrowSubscript<std::constant_wrapper<ThrowingSubscript{}>, std::constant_wrapper<1>>,
               "the subscript expression is still nothrow because the constexpr path is taken");
 
+template <class T>
+struct MustBeInt {
+  static_assert(std::same_as<T, int>);
+};
+
+struct Poison {
+  template <class T>
+  constexpr auto operator[](T) const noexcept -> MustBeInt<T> {
+    return {};
+  }
+};
+
 constexpr bool test() {
   {
     // with runtime param
@@ -172,6 +184,12 @@ constexpr bool test() {
     static_assert(result == 2);
   }
 
+  {
+    using T = std::constant_wrapper<Poison{}>;
+    [[maybe_unused]] std::same_as<std::constant_wrapper<MustBeInt<int>{}>> decltype(auto) result =
+        T::operator[](std::cw<5>);
+  }
+
   return true;
 }
 


        


More information about the libcxx-commits mailing list