[libcxx-commits] [libcxx] 07a9dae - [libc++] Implement LWG3476: Remove incorrect decay-copy in std::async and add QoI static_assert improvements (#173363)
via libcxx-commits
libcxx-commits at lists.llvm.org
Thu Dec 25 07:02:04 PST 2025
Author: Marcell Leleszi
Date: 2025-12-25T23:01:59+08:00
New Revision: 07a9dae40672a45149da66f9046034869056170e
URL: https://github.com/llvm/llvm-project/commit/07a9dae40672a45149da66f9046034869056170e
DIFF: https://github.com/llvm/llvm-project/commit/07a9dae40672a45149da66f9046034869056170e.diff
LOG: [libc++] Implement LWG3476: Remove incorrect decay-copy in std::async and add QoI static_assert improvements (#173363)
Fixes https://github.com/llvm/llvm-project/issues/104307
This patch implements LWG3476 by removing the incorrect decay-copy in
std::async. The decay-copy was being applied twice, once explicitly via
_LIBCPP_AUTO_CAST and once in __async_func's tuple constructor.
(https://github.com/llvm/llvm-project/issues/143828)
It also adds static_assert mandates to std::thread and std::async (which
were already implicitly enforced) and expands test coverage.
Added:
libcxx/test/std/thread/thread.jthread/cons.verify.cpp
libcxx/test/std/thread/thread.threads/thread.thread.class/thread.thread.constr/F.verify.cpp
Modified:
libcxx/docs/Status/Cxx23Issues.csv
libcxx/include/__thread/thread.h
libcxx/include/future
libcxx/test/std/thread/futures/futures.async/async.pass.cpp
libcxx/test/std/thread/futures/futures.async/async.verify.cpp
libcxx/test/std/thread/thread.jthread/cons.func.token.pass.cpp
libcxx/test/std/thread/thread.threads/thread.thread.class/thread.thread.constr/F.pass.cpp
Removed:
################################################################################
diff --git a/libcxx/docs/Status/Cxx23Issues.csv b/libcxx/docs/Status/Cxx23Issues.csv
index 389e1ad254a74..cc83a7da9c25e 100644
--- a/libcxx/docs/Status/Cxx23Issues.csv
+++ b/libcxx/docs/Status/Cxx23Issues.csv
@@ -45,7 +45,7 @@
"`LWG3472 <https://wg21.link/LWG3472>`__","``counted_iterator`` is missing preconditions","2020-11 (Virtual)","|Complete|","14","`#104304 <https://github.com/llvm/llvm-project/issues/104304>`__",""
"`LWG3473 <https://wg21.link/LWG3473>`__","Normative encouragement in non-normative note","2020-11 (Virtual)","|Complete|","15","`#104305 <https://github.com/llvm/llvm-project/issues/104305>`__",""
"`LWG3474 <https://wg21.link/LWG3474>`__","Nesting ``join_views`` is broken because of CTAD","2020-11 (Virtual)","|Complete|","15","`#104306 <https://github.com/llvm/llvm-project/issues/104306>`__",""
-"`LWG3476 <https://wg21.link/LWG3476>`__","``thread`` and ``jthread`` constructors require that the parameters be move-constructible but never move construct the parameters","2020-11 (Virtual)","","","`#104307 <https://github.com/llvm/llvm-project/issues/104307>`__",""
+"`LWG3476 <https://wg21.link/LWG3476>`__","``thread`` and ``jthread`` constructors require that the parameters be move-constructible but never move construct the parameters","2020-11 (Virtual)","|Complete|","22","`#104307 <https://github.com/llvm/llvm-project/issues/104307>`__",""
"`LWG3477 <https://wg21.link/LWG3477>`__","Simplify constraints for ``semiregular-box``","2020-11 (Virtual)","|Complete|","13","`#104308 <https://github.com/llvm/llvm-project/issues/104308>`__",""
"`LWG3482 <https://wg21.link/LWG3482>`__","``drop_view``'s const begin should additionally require ``sized_range``","2020-11 (Virtual)","|Complete|","14","`#104309 <https://github.com/llvm/llvm-project/issues/104309>`__",""
"`LWG3483 <https://wg21.link/LWG3483>`__","``transform_view::iterator``'s
diff erence is overconstrained","2020-11 (Virtual)","|Complete|","14","`#104310 <https://github.com/llvm/llvm-project/issues/104310>`__",""
diff --git a/libcxx/include/__thread/thread.h b/libcxx/include/__thread/thread.h
index 561f092ddb7c0..b2f51aa816c10 100644
--- a/libcxx/include/__thread/thread.h
+++ b/libcxx/include/__thread/thread.h
@@ -25,6 +25,8 @@
#include <__thread/support.h>
#include <__type_traits/decay.h>
#include <__type_traits/enable_if.h>
+#include <__type_traits/invoke.h>
+#include <__type_traits/is_constructible.h>
#include <__type_traits/is_same.h>
#include <__type_traits/remove_cvref.h>
#include <__utility/forward.h>
@@ -205,6 +207,10 @@ class _LIBCPP_EXPORTED_FROM_ABI thread {
# ifndef _LIBCPP_CXX03_LANG
template <class _Fp, class... _Args, __enable_if_t<!is_same<__remove_cvref_t<_Fp>, thread>::value, int> = 0>
_LIBCPP_HIDE_FROM_ABI explicit thread(_Fp&& __f, _Args&&... __args) {
+ static_assert(is_constructible<__decay_t<_Fp>, _Fp>::value, "");
+ static_assert(_And<is_constructible<__decay_t<_Args>, _Args>...>::value, "");
+ static_assert(__is_invocable_v<__decay_t<_Fp>, __decay_t<_Args>...>, "");
+
typedef unique_ptr<__thread_struct> _TSPtr;
_TSPtr __tsp(new __thread_struct);
typedef tuple<_TSPtr, __decay_t<_Fp>, __decay_t<_Args>...> _Gp;
diff --git a/libcxx/include/future b/libcxx/include/future
index c249bc5e7938f..4c0339f64de9c 100644
--- a/libcxx/include/future
+++ b/libcxx/include/future
@@ -397,6 +397,7 @@ template <class R, class Alloc> struct uses_allocator<packaged_task<R>, Alloc>;
# include <__type_traits/decay.h>
# include <__type_traits/enable_if.h>
# include <__type_traits/invoke.h>
+# include <__type_traits/is_constructible.h>
# include <__type_traits/is_same.h>
# include <__type_traits/remove_cvref.h>
# include <__type_traits/remove_reference.h>
@@ -1828,8 +1829,9 @@ class _LIBCPP_HIDDEN __async_func {
public:
using _Rp _LIBCPP_NODEBUG = __invoke_result_t<_Fp, _Args...>;
- _LIBCPP_HIDE_FROM_ABI explicit __async_func(_Fp&& __f, _Args&&... __args)
- : __f_(std::move(__f), std::move(__args)...) {}
+ template <class _Gp, class... _BArgs>
+ _LIBCPP_HIDE_FROM_ABI explicit __async_func(_Gp&& __g, _BArgs&&... __bargs)
+ : __f_(std::forward<_Gp>(__g), std::forward<_BArgs>(__bargs)...) {}
_LIBCPP_HIDE_FROM_ABI __async_func(__async_func&& __f) : __f_(std::move(__f.__f_)) {}
@@ -1847,6 +1849,10 @@ inline _LIBCPP_HIDE_FROM_ABI bool __does_policy_contain(launch __policy, launch
template <class _Fp, class... _Args>
[[__nodiscard__]] _LIBCPP_HIDE_FROM_ABI future<__invoke_result_t<__decay_t<_Fp>, __decay_t<_Args>...> >
async(launch __policy, _Fp&& __f, _Args&&... __args) {
+ static_assert(is_constructible<__decay_t<_Fp>, _Fp>::value, "");
+ static_assert(_And<is_constructible<__decay_t<_Args>, _Args>...>::value, "");
+ static_assert(__is_invocable_v<__decay_t<_Fp>, __decay_t<_Args>...>, "");
+
typedef __async_func<__decay_t<_Fp>, __decay_t<_Args>...> _BF;
typedef typename _BF::_Rp _Rp;
@@ -1854,8 +1860,7 @@ async(launch __policy, _Fp&& __f, _Args&&... __args) {
try {
# endif
if (__does_policy_contain(__policy, launch::async))
- return std::__make_async_assoc_state<_Rp>(
- _BF(_LIBCPP_AUTO_CAST(std::forward<_Fp>(__f)), _LIBCPP_AUTO_CAST(std::forward<_Args>(__args))...));
+ return std::__make_async_assoc_state<_Rp>(_BF(std::forward<_Fp>(__f), std::forward<_Args>(__args)...));
# if _LIBCPP_HAS_EXCEPTIONS
} catch (...) {
if (__policy == launch::async)
@@ -1864,8 +1869,7 @@ async(launch __policy, _Fp&& __f, _Args&&... __args) {
# endif
if (__does_policy_contain(__policy, launch::deferred))
- return std::__make_deferred_assoc_state<_Rp>(
- _BF(_LIBCPP_AUTO_CAST(std::forward<_Fp>(__f)), _LIBCPP_AUTO_CAST(std::forward<_Args>(__args))...));
+ return std::__make_deferred_assoc_state<_Rp>(_BF(std::forward<_Fp>(__f), std::forward<_Args>(__args)...));
return future<_Rp>{};
}
diff --git a/libcxx/test/std/thread/futures/futures.async/async.pass.cpp b/libcxx/test/std/thread/futures/futures.async/async.pass.cpp
index 109372b50a311..81232492c2545 100644
--- a/libcxx/test/std/thread/futures/futures.async/async.pass.cpp
+++ b/libcxx/test/std/thread/futures/futures.async/async.pass.cpp
@@ -155,5 +155,30 @@ int main(int, char**)
try { f.get(); assert (false); } catch ( int ) {}
}
#endif
+ {
+ class CopyOnly {
+ public:
+ CopyOnly() {}
+ CopyOnly(const CopyOnly&) = default;
+ CopyOnly(CopyOnly&&) = delete;
+
+ void operator()(const CopyOnly&) const {}
+ };
+ CopyOnly c;
+ std::future<void> f = std::async(c, c);
+ f.wait();
+ }
+ {
+ class MoveOnly {
+ public:
+ MoveOnly() {}
+ MoveOnly(const MoveOnly&) = delete;
+ MoveOnly(MoveOnly&&) = default;
+
+ void operator()(MoveOnly&&) const {}
+ };
+ std::future<void> f = std::async(MoveOnly{}, MoveOnly{});
+ f.wait();
+ }
return 0;
}
diff --git a/libcxx/test/std/thread/futures/futures.async/async.verify.cpp b/libcxx/test/std/thread/futures/futures.async/async.verify.cpp
index e47ad82e79980..2cb15a79ae089 100644
--- a/libcxx/test/std/thread/futures/futures.async/async.verify.cpp
+++ b/libcxx/test/std/thread/futures/futures.async/async.verify.cpp
@@ -27,4 +27,75 @@ int foo (int x) { return x; }
void f() {
std::async( foo, 3); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
std::async(std::launch::async, foo, 3); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
+
+ { // Mandates: is_constructible_v<decay_t<F>, F>
+ struct F {
+ F() = default;
+ F(const F&) = delete;
+ F(F&&) = default;
+
+ void operator()() {}
+ };
+
+ F f;
+ static_cast<void>(std::async(f));
+ // expected-error@*:* {{static assertion failed}}
+ // expected-error@*:* {{no matching constructor for initialization}}
+ // expected-error@*:* 0-1 {{call to deleted constructor}}
+ }
+
+ { // Mandates: is_constructible_v<decay_t<F>, F>
+ struct F {
+ F() = default;
+ F(const F&) = default;
+ F(F&&) = delete;
+
+ void operator()() {}
+ };
+
+ static_cast<void>(std::async(F{}));
+ // expected-error@*:* {{static assertion failed}}
+ // expected-error@*:* {{no matching constructor for initialization}}
+ // expected-error@*:* 0-1 {{call to deleted constructor}}
+ }
+
+ { // Mandates: (is_constructible_v<decay_t<Args>, Args> && ...)
+ struct Arg {
+ Arg() = default;
+ Arg(const Arg&) = delete;
+ Arg(Arg&&) = default;
+ };
+
+ struct F {
+ void operator()(const Arg&) const {}
+ };
+
+ Arg arg;
+ static_cast<void>(std::async(F{}, arg));
+ // expected-error@*:* {{static assertion failed}}
+ // expected-error@*:* 0-1 {{call to deleted constructor}}
+ }
+
+ { // Mandates: (is_constructible_v<decay_t<Args>, Args> && ...)
+ struct Arg {
+ Arg() = default;
+ Arg(const Arg&) = default;
+ Arg(Arg&&) = delete;
+ };
+
+ struct F {
+ void operator()(const Arg&) const {}
+ };
+
+ static_cast<void>(std::async(F{}, Arg{}));
+ // expected-error@*:* {{static assertion failed}}
+ // expected-error@*:* 0-1 {{call to deleted constructor}}
+ }
+
+ { // Mandates: is_invocable_v<decay_t<F>, decay_t<Args>...>
+ struct F {};
+
+ static_cast<void>(std::async(F{}));
+ // expected-error@*:* {{no matching function}}
+ }
}
diff --git a/libcxx/test/std/thread/thread.jthread/cons.func.token.pass.cpp b/libcxx/test/std/thread/thread.jthread/cons.func.token.pass.cpp
index e8a57cc684138..9342e94560491 100644
--- a/libcxx/test/std/thread/thread.jthread/cons.func.token.pass.cpp
+++ b/libcxx/test/std/thread/thread.jthread/cons.func.token.pass.cpp
@@ -155,5 +155,30 @@ int main(int, char**) {
//
// Unfortunately, this is extremely hard to test portably so we don't have a test for this error condition right now.
+ {
+ class CopyOnly {
+ public:
+ CopyOnly() {}
+ CopyOnly(const CopyOnly&) = default;
+ CopyOnly(CopyOnly&&) = delete;
+
+ void operator()(const CopyOnly&) const {}
+ };
+ CopyOnly c;
+ std::jthread t(c, c);
+ }
+
+ {
+ class MoveOnly {
+ public:
+ MoveOnly() {}
+ MoveOnly(const MoveOnly&) = delete;
+ MoveOnly(MoveOnly&&) = default;
+
+ void operator()(MoveOnly&&) const {}
+ };
+ std::jthread t(MoveOnly{}, MoveOnly{});
+ }
+
return 0;
}
diff --git a/libcxx/test/std/thread/thread.jthread/cons.verify.cpp b/libcxx/test/std/thread/thread.jthread/cons.verify.cpp
new file mode 100644
index 0000000000000..9048f3f48bb76
--- /dev/null
+++ b/libcxx/test/std/thread/thread.jthread/cons.verify.cpp
@@ -0,0 +1,96 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// REQUIRES: std-at-least-c++20
+
+// <jthread>
+
+// class jthread
+
+// template<class F, class... Args>
+// explicit jthread(F&& f, Args&&... args);
+
+#include <thread>
+
+void test() {
+ { // Mandates: is_constructible_v<decay_t<F>, F>
+ struct F {
+ F() = default;
+ F(const F&) = delete;
+ F(F&&) = default;
+
+ void operator()() {}
+ };
+
+ F f;
+ std::jthread t(f);
+ // expected-error@*:* 2 {{static assertion failed}}
+ // expected-error@*:* 0-2 {{call to deleted constructor}}
+ // expected-error@*:* {{no matching constructor for initialization}}
+ }
+
+ { // Mandates: is_constructible_v<decay_t<F>, F>
+ struct F {
+ F() = default;
+ F(const F&) = default;
+ F(F&&) = delete;
+
+ void operator()() {}
+ };
+
+ std::jthread t(F{});
+ // expected-error@*:* 2 {{static assertion failed}}
+ // expected-error@*:* 0-2 {{call to deleted constructor}}
+ // expected-error@*:* {{no matching constructor for initialization}}
+ }
+
+ { // Mandates: (is_constructible_v<decay_t<Args>, Args> && ...)
+ struct Arg {
+ Arg() = default;
+ Arg(const Arg&) = delete;
+ Arg(Arg&&) = default;
+ };
+
+ struct F {
+ void operator()(const Arg&) const {}
+ };
+
+ Arg arg;
+ std::jthread t(F{}, arg);
+ // expected-error@*:* 2 {{static assertion failed}}
+ // expected-error@*:* 0-1 {{call to deleted constructor}}
+ // expected-error@*:* {{no matching constructor for initialization}}
+ }
+
+ { // Mandates: (is_constructible_v<decay_t<Args>, Args> && ...)
+ struct Arg {
+ Arg() = default;
+ Arg(const Arg&) = default;
+ Arg(Arg&&) = delete;
+ };
+
+ struct F {
+ void operator()(const Arg&) const {}
+ };
+
+ std::jthread t(F{}, Arg{});
+ // expected-error@*:* 2 {{static assertion failed}}
+ // expected-error@*:* 0-1 {{call to deleted constructor}}
+ // expected-error@*:* {{no matching constructor for initialization}}
+ }
+
+ { // Mandates: is_invocable_v<decay_t<F>, decay_t<Args>...>
+ struct F {};
+
+ std::jthread t(F{});
+ // expected-error@*:* 2 {{static assertion failed}}
+ // expected-error@*:* 0-1 {{no matching function for call to '__invoke'}}
+ // expected-error@*:* 0-1 {{attempt to use a deleted function}}
+ }
+}
diff --git a/libcxx/test/std/thread/thread.threads/thread.thread.class/thread.thread.constr/F.pass.cpp b/libcxx/test/std/thread/thread.threads/thread.thread.class/thread.thread.constr/F.pass.cpp
index 1c520652ba229..6f7e5291b576e 100644
--- a/libcxx/test/std/thread/thread.threads/thread.thread.class/thread.thread.constr/F.pass.cpp
+++ b/libcxx/test/std/thread/thread.threads/thread.thread.class/thread.thread.constr/F.pass.cpp
@@ -103,6 +103,15 @@ class MoveOnly
}
};
+class CopyOnly {
+public:
+ CopyOnly() {}
+ CopyOnly(const CopyOnly&) = default;
+ CopyOnly(CopyOnly&&) = delete;
+
+ void operator()(const CopyOnly&) const {}
+};
+
#endif
// Test throwing std::bad_alloc
@@ -212,6 +221,11 @@ int main(int, char**)
std::thread t = std::thread(MoveOnly(), MoveOnly());
t.join();
}
+ {
+ CopyOnly c;
+ std::thread t(c, c);
+ t.join();
+ }
#endif
return 0;
diff --git a/libcxx/test/std/thread/thread.threads/thread.thread.class/thread.thread.constr/F.verify.cpp b/libcxx/test/std/thread/thread.threads/thread.thread.class/thread.thread.constr/F.verify.cpp
new file mode 100644
index 0000000000000..51815be23a4c4
--- /dev/null
+++ b/libcxx/test/std/thread/thread.threads/thread.thread.class/thread.thread.constr/F.verify.cpp
@@ -0,0 +1,94 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// REQUIRES: std-at-least-c++11
+
+// <thread>
+
+// class thread
+
+// template<class F, class... Args>
+// explicit thread(F&& f, Args&&... args);
+
+#include <thread>
+
+void test() {
+ { // Mandates: is_constructible_v<decay_t<F>, F>
+ struct F {
+ F() = default;
+ F(const F&) = delete;
+ F(F&&) = default;
+
+ void operator()() {}
+ };
+
+ F f;
+ std::thread t(f);
+ // expected-error@*:* {{static assertion failed}}
+ // expected-error@*:* 0-1 {{call to deleted constructor}}
+ // expected-error@*:* {{no matching constructor for initialization}}
+ }
+
+ { // Mandates: is_constructible_v<decay_t<F>, F>
+ struct F {
+ F() = default;
+ F(const F&) = default;
+ F(F&&) = delete;
+
+ void operator()() {}
+ };
+
+ std::thread t(F{});
+ // expected-error@*:* {{static assertion failed}}
+ // expected-error@*:* 0-1 {{call to deleted constructor}}
+ // expected-error@*:* {{no matching constructor for initialization}}
+ }
+
+ { // Mandates: (is_constructible_v<decay_t<Args>, Args> && ...)
+ struct Arg {
+ Arg() = default;
+ Arg(const Arg&) = delete;
+ Arg(Arg&&) = default;
+ };
+
+ struct F {
+ void operator()(const Arg&) const {}
+ };
+
+ Arg arg;
+ std::thread t(F{}, arg);
+ // expected-error@*:* {{static assertion failed}}
+ // expected-error@*:* {{no matching constructor for initialization}}
+ }
+
+ { // Mandates: (is_constructible_v<decay_t<Args>, Args> && ...)
+ struct Arg {
+ Arg() = default;
+ Arg(const Arg&) = default;
+ Arg(Arg&&) = delete;
+ };
+
+ struct F {
+ void operator()(const Arg&) const {}
+ };
+
+ std::thread t(F{}, Arg{});
+ // expected-error@*:* {{static assertion failed}}
+ // expected-error@*:* {{no matching constructor for initialization}}
+ }
+
+ { // Mandates: is_invocable_v<decay_t<F>, decay_t<Args>...>
+ struct F {};
+
+ std::thread t(F{});
+ // expected-error@*:* {{static assertion failed}}
+ // expected-error@*:* 0-1 {{no matching function for call to '__invoke'}}
+ // expected-error@*:* 0-1 {{attempt to use a deleted function}}
+ }
+}
More information about the libcxx-commits
mailing list