[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