[libcxx-commits] [libcxx] [libc++] P2255R2: Restrict `INVOKE`-related components (PR #174302)

A. Jiang via libcxx-commits libcxx-commits at lists.llvm.org
Sat Jan 3 17:41:45 PST 2026


https://github.com/frederick-vs-ja updated https://github.com/llvm/llvm-project/pull/174302

>From 8d12004612b84f4af568916e1685a550a4df4db9 Mon Sep 17 00:00:00 2001
From: "A. Jiang" <de34 at live.cn>
Date: Tue, 30 Dec 2025 16:14:11 +0800
Subject: [PATCH] [libc++] P2255R2: Restrict `INVOKE`-related components

Remarks:
- `packaged_task` and `visit` only have _Mandates_ strengthened. Since
C++26, such _Mandates_ can rely on core language changes (P2748R5). So
this patch only adds "library workaround" before C++26.
- `move_only_function`, `copyable_function`, and `function_ref` are not
implemented. Restrictions should be added when implementing them.
---
 libcxx/include/__functional/bind.h            | 16 ++++----
 libcxx/include/__functional/invoke.h          |  6 ---
 libcxx/include/__type_traits/invoke.h         | 32 +++++++++++++--
 libcxx/include/future                         |  4 ++
 libcxx/include/variant                        |  5 +++
 .../futures.task.members/ctor_func.verify.cpp | 19 +++++++++
 .../func.bind.bind/bind_return_type.pass.cpp  | 40 ++++++++++++++++++-
 .../func.invoke/invoke_r.pass.cpp             |  3 ++
 .../func.invoke/invoke_r.temporary.verify.cpp | 31 --------------
 .../func.wrap.func.con/F.pass.cpp             | 20 +++++++++-
 .../meta.rel/is_invocable_r.compile.pass.cpp  | 11 ++++-
 .../is_invocable_r_v.compile.pass.cpp         | 11 ++++-
 .../visit_return_type.verify.cpp              | 19 +++++++++
 13 files changed, 165 insertions(+), 52 deletions(-)
 create mode 100644 libcxx/test/std/thread/futures/futures.task/futures.task.members/ctor_func.verify.cpp
 delete mode 100644 libcxx/test/std/utilities/function.objects/func.invoke/invoke_r.temporary.verify.cpp
 create mode 100644 libcxx/test/std/utilities/variant/variant.visit/visit_return_type.verify.cpp

diff --git a/libcxx/include/__functional/bind.h b/libcxx/include/__functional/bind.h
index cbe8660b821c1..f9a5a6cd4477a 100644
--- a/libcxx/include/__functional/bind.h
+++ b/libcxx/include/__functional/bind.h
@@ -245,18 +245,20 @@ class __bind_r : public __bind<_Fp, _BoundArgs...> {
   _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 explicit __bind_r(_Gp&& __f, _BA&&... __bound_args)
       : base(std::forward<_Gp>(__f), std::forward<_BA>(__bound_args)...) {}
 
-  template <
-      class... _Args,
-      __enable_if_t<is_convertible<typename __bind_return<_Fd, _Td, tuple<_Args&&...> >::type, result_type>::value ||
-                        is_void<_Rp>::value,
-                    int> = 0>
+  template < class... _Args,
+             __enable_if_t<
+                 __is_core_convertible_rejecting_temporary<typename __bind_return<_Fd, _Td, tuple<_Args&&...> >::type,
+                                                           result_type>::value ||
+                     is_void<_Rp>::value,
+                 int> = 0>
   _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 result_type operator()(_Args&&... __args) {
     return std::__invoke_r<_Rp>(static_cast<base&>(*this), std::forward<_Args>(__args)...);
   }
 
   template <class... _Args,
-            __enable_if_t<is_convertible<typename __bind_return<const _Fd, const _Td, tuple<_Args&&...> >::type,
-                                         result_type>::value ||
+            __enable_if_t<__is_core_convertible_rejecting_temporary<
+                              typename __bind_return<const _Fd, const _Td, tuple<_Args&&...> >::type,
+                              result_type>::value ||
                               is_void<_Rp>::value,
                           int> = 0>
   _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 result_type operator()(_Args&&... __args) const {
diff --git a/libcxx/include/__functional/invoke.h b/libcxx/include/__functional/invoke.h
index ab201e94206e1..53cea10432bec 100644
--- a/libcxx/include/__functional/invoke.h
+++ b/libcxx/include/__functional/invoke.h
@@ -39,12 +39,6 @@ invoke_r(_Fn&& __f, _Args&&... __args) noexcept(is_nothrow_invocable_r_v<_Result
   if constexpr (is_void_v<_Result>) {
     static_cast<void>(std::invoke(std::forward<_Fn>(__f), std::forward<_Args>(__args)...));
   } else {
-    // TODO: Use reference_converts_from_temporary_v once implemented
-    // using _ImplicitInvokeResult = invoke_result_t<_Fn, _Args...>;
-    // static_assert(!reference_converts_from_temporary_v<_Result, _ImplicitInvokeResult>,
-    static_assert(true,
-                  "Returning from invoke_r would bind a temporary object to the reference return type, "
-                  "which would result in a dangling reference.");
     return std::invoke(std::forward<_Fn>(__f), std::forward<_Args>(__args)...);
   }
 }
diff --git a/libcxx/include/__type_traits/invoke.h b/libcxx/include/__type_traits/invoke.h
index ba8202576593e..c258f9b45e9b9 100644
--- a/libcxx/include/__type_traits/invoke.h
+++ b/libcxx/include/__type_traits/invoke.h
@@ -12,6 +12,7 @@
 
 #include <__config>
 #include <__type_traits/conditional.h>
+#include <__type_traits/conjunction.h>
 #include <__type_traits/decay.h>
 #include <__type_traits/enable_if.h>
 #include <__type_traits/integral_constant.h>
@@ -22,6 +23,8 @@
 #include <__type_traits/is_same.h>
 #include <__type_traits/is_void.h>
 #include <__type_traits/nat.h>
+#include <__type_traits/negation.h>
+#include <__type_traits/reference_converts_from_temporary.h>
 #include <__type_traits/void_t.h>
 #include <__utility/declval.h>
 #include <__utility/forward.h>
@@ -68,6 +71,15 @@
 
 _LIBCPP_BEGIN_NAMESPACE_STD
 
+#if _LIBCPP_STD_VER >= 23
+template <class _Src, class _Dst>
+using __is_core_convertible_rejecting_temporary _LIBCPP_NODEBUG =
+    _And<__is_core_convertible<_Src, _Dst>, _Not<reference_converts_from_temporary<_Dst, _Src>>>;
+#else
+template <class _Src, class _Dst>
+using __is_core_convertible_rejecting_temporary _LIBCPP_NODEBUG = __is_core_convertible<_Src, _Dst>;
+#endif
+
 #if __has_builtin(__builtin_invoke)
 
 template <class, class... _Args>
@@ -107,7 +119,7 @@ inline const bool __is_invocable_r_impl = false;
 
 template <class _Ret, class... _Args>
 inline const bool __is_invocable_r_impl<_Ret, true, _Args...> =
-    __is_core_convertible<__invoke_result_t<_Args...>, _Ret>::value || is_void<_Ret>::value;
+    __is_core_convertible_rejecting_temporary<__invoke_result_t<_Args...>, _Ret>::value || is_void<_Ret>::value;
 
 template <class _Ret, class... _Args>
 inline const bool __is_invocable_r_v = __is_invocable_r_impl<_Ret, __is_invocable_v<_Args...>, _Args...>;
@@ -266,9 +278,10 @@ struct __invokable_r {
   // or incomplete array types as required by the standard.
   using _Result _LIBCPP_NODEBUG = decltype(__try_call<_Fp, _Args...>(0));
 
-  using type              = __conditional_t<_IsNotSame<_Result, __nat>::value,
-                                            __conditional_t<is_void<_Ret>::value, true_type, __is_core_convertible<_Result, _Ret> >,
-                                            false_type>;
+  using type = __conditional_t<
+      _IsNotSame<_Result, __nat>::value,
+      __conditional_t<is_void<_Ret>::value, true_type, __is_core_convertible_rejecting_temporary<_Result, _Ret> >,
+      false_type>;
   static const bool value = type::value;
 };
 template <class _Fp, class... _Args>
@@ -394,6 +407,17 @@ struct _LIBCPP_NO_SPECIALIZATIONS invoke_result : __invoke_result<_Fn, _Args...>
 template <class _Fn, class... _Args>
 using invoke_result_t = __invoke_result_t<_Fn, _Args...>;
 
+#  if _LIBCPP_STD_VER >= 23 && _LIBCPP_STD_VER < 26
+// __is_invocable_r_from_temporary is only used to reject binding reference to temporary in return statements.
+// Core language change in C++26 (per P2748R5) makes it unnecessary.
+template <class _Ret, class _Fn, class... _Args>
+inline constexpr bool __is_invocable_r_from_temporary = false;
+template <class _Ret, class _Fn, class... _Args>
+  requires __is_invocable_v<_Fn, _Args...>
+inline constexpr bool __is_invocable_r_from_temporary<_Ret, _Fn, _Args...> =
+    reference_converts_from_temporary_v<_Ret, __invoke_result_t<_Fn, _Args...>>;
+#  endif
+
 #endif
 
 _LIBCPP_END_NAMESPACE_STD
diff --git a/libcxx/include/future b/libcxx/include/future
index 4c0339f64de9c..aaa7f80adbc4f 100644
--- a/libcxx/include/future
+++ b/libcxx/include/future
@@ -1504,6 +1504,10 @@ template <class _Fp>
 __packaged_task_function<_Rp(_ArgTypes...)>::__packaged_task_function(_Fp&& __f) : __f_(nullptr) {
   typedef __libcpp_remove_reference_t<__decay_t<_Fp> > _FR;
   typedef __packaged_task_func<_FR, allocator<_FR>, _Rp(_ArgTypes...)> _FF;
+#    if _LIBCPP_STD_VER >= 23 && _LIBCPP_STD_VER < 26
+  static_assert(
+      !__is_invocable_r_from_temporary<_Rp, _FR&, _ArgTypes...>, "returning reference to local temporary object");
+#    endif
   if (sizeof(_FF) <= sizeof(__buf_)) {
     ::new ((void*)&__buf_) _FF(std::forward<_Fp>(__f));
     __f_ = (__base*)&__buf_;
diff --git a/libcxx/include/variant b/libcxx/include/variant
index b93009b3353cb..17585902a144e 100644
--- a/libcxx/include/variant
+++ b/libcxx/include/variant
@@ -630,6 +630,11 @@ private:
       if constexpr (is_void_v<_Rp>) {
         std::__invoke(std::forward<_Visitor>(__visitor), std::forward<_Alts>(__alts).__value...);
       } else {
+#      if _LIBCPP_STD_VER >= 23 && _LIBCPP_STD_VER < 26
+        static_assert(
+            !__is_invocable_r_from_temporary<_Rp, _Visitor, decltype((std::forward<_Alts>(__alts).__value))...>,
+            "returning reference to local temporary object");
+#      endif
         return std::__invoke(std::forward<_Visitor>(__visitor), std::forward<_Alts>(__alts).__value...);
       }
     }
diff --git a/libcxx/test/std/thread/futures/futures.task/futures.task.members/ctor_func.verify.cpp b/libcxx/test/std/thread/futures/futures.task/futures.task.members/ctor_func.verify.cpp
new file mode 100644
index 0000000000000..96c52e4047766
--- /dev/null
+++ b/libcxx/test/std/thread/futures/futures.task/futures.task.members/ctor_func.verify.cpp
@@ -0,0 +1,19 @@
+//===----------------------------------------------------------------------===//
+//
+// 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++23
+
+#include <future>
+
+// Verify that binding returned reference to local temporary object is rejected
+// (per P2255R2 in library, P2748R5 in core langauge).
+
+void test() {
+  // expected-error at future:* {{returning reference to local temporary object}}
+  (void)std::packaged_task<const int&(int)>{[](int n) { return n; }};
+}
diff --git a/libcxx/test/std/utilities/function.objects/bind/func.bind/func.bind.bind/bind_return_type.pass.cpp b/libcxx/test/std/utilities/function.objects/bind/func.bind/func.bind.bind/bind_return_type.pass.cpp
index 0ba6e01ee9e9b..bd45e9174cbc6 100644
--- a/libcxx/test/std/utilities/function.objects/bind/func.bind/func.bind.bind/bind_return_type.pass.cpp
+++ b/libcxx/test/std/utilities/function.objects/bind/func.bind/func.bind.bind/bind_return_type.pass.cpp
@@ -116,6 +116,40 @@ void do_test_r(Fn* func) {
     }
 }
 
+// P2255R2 disallows binding returned reference to temporary since C++23.
+struct ConditionalTemporaryReturner {
+  constexpr const int& operator()(std::false_type) const { return std::integral_constant<int, 42>::value; }
+  constexpr int operator()(std::true_type) const { return 42; }
+};
+
+TEST_CONSTEXPR_CXX20 bool test_temporary_binding() {
+#if TEST_STD_VER >= 23
+  constexpr bool can_bind_to_temporary = false;
+#else
+  constexpr bool can_bind_to_temporary = true;
+#endif
+
+  auto bound1 = std::bind<const int&>(ConditionalTemporaryReturner{}, std::false_type{});
+
+  static_assert(CheckCall<decltype(bound1)>(), "");
+  static_assert(CheckCall<decltype(bound1)>(), "");
+  static_assert(CheckCall<const decltype(bound1)>(), "");
+  static_assert(CheckCall<const decltype(bound1)>(), "");
+
+  assert(bound1() == 42);
+  assert((&bound1() == &std::integral_constant<int, 42>::value));
+
+  auto bound2 = std::bind<const int&>(ConditionalTemporaryReturner{}, std::true_type{});
+  (void)bound2;
+
+  static_assert(CheckCall<decltype(bound2)>() == can_bind_to_temporary, "");
+  static_assert(CheckCall<decltype(bound2)>() == can_bind_to_temporary, "");
+  static_assert(CheckCall<const decltype(bound2)>() == can_bind_to_temporary, "");
+  static_assert(CheckCall<const decltype(bound2)>() == can_bind_to_temporary, "");
+
+  return true;
+}
+
 int main(int, char**)
 {
     do_test<int>(return_value);
@@ -130,6 +164,10 @@ int main(int, char**)
     do_test_r<long>(return_rvalue);
     do_test_r<long>(return_const_rvalue);
 
+    test_temporary_binding();
+#if TEST_STD_VER >= 20
+    static_assert(test_temporary_binding());
+#endif
 
-  return 0;
+    return 0;
 }
diff --git a/libcxx/test/std/utilities/function.objects/func.invoke/invoke_r.pass.cpp b/libcxx/test/std/utilities/function.objects/func.invoke/invoke_r.pass.cpp
index 8a08d3636d1e2..c0d5859960b41 100644
--- a/libcxx/test/std/utilities/function.objects/func.invoke/invoke_r.pass.cpp
+++ b/libcxx/test/std/utilities/function.objects/func.invoke/invoke_r.pass.cpp
@@ -43,6 +43,9 @@ constexpr bool test() {
         static_assert(!can_invoke_r<char*, decltype(f)>);        // missing argument
         static_assert(!can_invoke_r<int*,  decltype(f), int>);   // incompatible return type
         static_assert(!can_invoke_r<void,  decltype(f), void*>); // discard return type, invalid argument type
+
+        static_assert(!can_invoke_r<char* const&, decltype(f), int>); // binding to temporary
+        static_assert(!can_invoke_r<char*&&, decltype(f), int>);      // same as above
     }
 
     // Make sure invoke_r has the right noexcept specification
diff --git a/libcxx/test/std/utilities/function.objects/func.invoke/invoke_r.temporary.verify.cpp b/libcxx/test/std/utilities/function.objects/func.invoke/invoke_r.temporary.verify.cpp
deleted file mode 100644
index 2506ea0afc811..0000000000000
--- a/libcxx/test/std/utilities/function.objects/func.invoke/invoke_r.temporary.verify.cpp
+++ /dev/null
@@ -1,31 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// 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
-
-// <functional>
-
-// template<class R, class F, class... Args>
-// constexpr R invoke_r(F&& f, Args&&... args)              // C++23
-//     noexcept(is_nothrow_invocable_r_v<R, F, Args...>);
-//
-// Make sure that we diagnose when std::invoke_r is used with a return type that
-// would yield a dangling reference to a temporary.
-
-// TODO: We currently can't diagnose because we don't implement reference_converts_from_temporary.
-// XFAIL: *
-
-#include <functional>
-#include <cassert>
-
-#include "test_macros.h"
-
-void f() {
-    auto func = []() -> int { return 0; };
-    std::invoke_r<int&&>(func); // expected-error {{Returning from invoke_r would bind a temporary object}}
-}
diff --git a/libcxx/test/std/utilities/function.objects/func.wrap/func.wrap.func/func.wrap.func.con/F.pass.cpp b/libcxx/test/std/utilities/function.objects/func.wrap/func.wrap.func/func.wrap.func.con/F.pass.cpp
index 92409577d60de..2eaa032f3e044 100644
--- a/libcxx/test/std/utilities/function.objects/func.wrap/func.wrap.func/func.wrap.func.con/F.pass.cpp
+++ b/libcxx/test/std/utilities/function.objects/func.wrap/func.wrap.func/func.wrap.func.con/F.pass.cpp
@@ -114,6 +114,24 @@ int main(int, char**)
         static_assert(!std::is_constructible<Fn, RValueCallable>::value, "");
     }
 #endif
+    {
+      // P2255R2 made binding to temporary ill-formed since C++23.
+      struct RetPrvalue {
+        int operator()(int) const { return 42; }
+      };
+      typedef std::function<const int&(int)> Fn;
+#if TEST_STD_VER >= 23
+      static_assert(!std::is_constructible_v<Fn, RetPrvalue>);
+      static_assert(!std::is_constructible_v<Fn, RetPrvalue&>);
+      static_assert(!std::is_constructible_v<Fn, const RetPrvalue>);
+      static_assert(!std::is_constructible_v<Fn, const RetPrvalue&>);
+#else
+      static_assert(std::is_constructible<Fn, RetPrvalue>::value, "");
+      static_assert(std::is_constructible<Fn, RetPrvalue&>::value, "");
+      static_assert(std::is_constructible<Fn, const RetPrvalue>::value, "");
+      static_assert(std::is_constructible<Fn, const RetPrvalue&>::value, "");
+#endif
+    }
 
-  return 0;
+    return 0;
 }
diff --git a/libcxx/test/std/utilities/meta/meta.rel/is_invocable_r.compile.pass.cpp b/libcxx/test/std/utilities/meta/meta.rel/is_invocable_r.compile.pass.cpp
index 778b24d99e91e..46723a14ae495 100644
--- a/libcxx/test/std/utilities/meta/meta.rel/is_invocable_r.compile.pass.cpp
+++ b/libcxx/test/std/utilities/meta/meta.rel/is_invocable_r.compile.pass.cpp
@@ -12,6 +12,8 @@
 
 #include <type_traits>
 
+#include "test_macros.h"
+
 // Non-invocable types
 
 static_assert(!std::is_invocable_r<void, void>::value);
@@ -55,10 +57,17 @@ static_assert(std::is_invocable_r<const volatile void, decltype(Return<int>)>::v
 static_assert(std::is_invocable_r<char, decltype(Return<int>)>::value);
 static_assert(std::is_invocable_r<const int*, decltype(Return<int*>)>::value);
 static_assert(std::is_invocable_r<void*, decltype(Return<int*>)>::value);
-static_assert(std::is_invocable_r<const int&, decltype(Return<int>)>::value);
 static_assert(std::is_invocable_r<const int&, decltype(Return<int&>)>::value);
 static_assert(std::is_invocable_r<const int&, decltype(Return<int&&>)>::value);
+
+// P2255R2 made binding to temporary ill-formed since C++23.
+#if TEST_STD_VER >= 23
+static_assert(!std::is_invocable_r<const int&, decltype(Return<int>)>::value);
+static_assert(!std::is_invocable_r<const char&, decltype(Return<int>)>::value);
+#else
+static_assert(std::is_invocable_r<const int&, decltype(Return<int>)>::value);
 static_assert(std::is_invocable_r<const char&, decltype(Return<int>)>::value);
+#endif
 
 // But not a result type where the conversion doesn't work.
 static_assert(!std::is_invocable_r<int, decltype(Return<void>)>::value);
diff --git a/libcxx/test/std/utilities/meta/meta.rel/is_invocable_r_v.compile.pass.cpp b/libcxx/test/std/utilities/meta/meta.rel/is_invocable_r_v.compile.pass.cpp
index 32530f2c20865..cdf51a472891f 100644
--- a/libcxx/test/std/utilities/meta/meta.rel/is_invocable_r_v.compile.pass.cpp
+++ b/libcxx/test/std/utilities/meta/meta.rel/is_invocable_r_v.compile.pass.cpp
@@ -12,6 +12,8 @@
 
 #include <type_traits>
 
+#include "test_macros.h"
+
 // Non-invocable types
 
 static_assert(!std::is_invocable_r_v<void, void>);
@@ -55,10 +57,17 @@ static_assert(std::is_invocable_r_v<const volatile void, decltype(Return<int>)>)
 static_assert(std::is_invocable_r_v<char, decltype(Return<int>)>);
 static_assert(std::is_invocable_r_v<const int*, decltype(Return<int*>)>);
 static_assert(std::is_invocable_r_v<void*, decltype(Return<int*>)>);
-static_assert(std::is_invocable_r_v<const int&, decltype(Return<int>)>);
 static_assert(std::is_invocable_r_v<const int&, decltype(Return<int&>)>);
 static_assert(std::is_invocable_r_v<const int&, decltype(Return<int&&>)>);
+
+// P2255R2 made binding to temporary ill-formed since C++23.
+#if TEST_STD_VER >= 23
+static_assert(!std::is_invocable_r_v<const int&, decltype(Return<int>)>);
+static_assert(!std::is_invocable_r_v<const char&, decltype(Return<int>)>);
+#else
+static_assert(std::is_invocable_r_v<const int&, decltype(Return<int>)>);
 static_assert(std::is_invocable_r_v<const char&, decltype(Return<int>)>);
+#endif
 
 // But not a result type where the conversion doesn't work.
 static_assert(!std::is_invocable_r_v<int, decltype(Return<void>)>);
diff --git a/libcxx/test/std/utilities/variant/variant.visit/visit_return_type.verify.cpp b/libcxx/test/std/utilities/variant/variant.visit/visit_return_type.verify.cpp
new file mode 100644
index 0000000000000..644fe1c4a4d4a
--- /dev/null
+++ b/libcxx/test/std/utilities/variant/variant.visit/visit_return_type.verify.cpp
@@ -0,0 +1,19 @@
+//===----------------------------------------------------------------------===//
+//
+// 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++23
+
+#include <variant>
+
+// Verify that binding returned reference to local temporary object is rejected
+// (per P2255R2 in library, P2748R5 in core langauge).
+
+void test() {
+  // expected-error at variant:* {{returning reference to local temporary object}}
+  std::visit<const int&>([](char c) { return c; }, std::variant<char>{'*'});
+}



More information about the libcxx-commits mailing list