[libcxx-commits] [libcxx] [libc++] Implement LWG3476: Add mandates to thread, jthread, and async (PR #173363)

Marcell Leleszi via libcxx-commits libcxx-commits at lists.llvm.org
Tue Dec 23 04:46:19 PST 2025


https://github.com/mleleszi updated https://github.com/llvm/llvm-project/pull/173363

>From 376e73497074b14582a113776519abbb2b726258 Mon Sep 17 00:00:00 2001
From: mleleszi <mleleszi at icloud.com>
Date: Sun, 21 Dec 2025 20:10:53 +0100
Subject: [PATCH 1/3] Add mandates for thread

---
 libcxx/include/__thread/thread.h              |  8 +++
 libcxx/include/future                         |  7 +++
 .../futures/futures.async/async.verify.cpp    | 15 +++++
 .../thread.thread.constr/F.pass.cpp           | 14 +++++
 .../thread.thread.constr/F.verify.cpp         | 63 +++++++++++++++++++
 5 files changed, 107 insertions(+)
 create mode 100644 libcxx/test/std/thread/thread.threads/thread.thread.class/thread.thread.constr/F.verify.cpp

diff --git a/libcxx/include/__thread/thread.h b/libcxx/include/__thread/thread.h
index 561f092ddb7c0..24b4caa7f0d2f 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,12 @@ 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) {
+#    if _LIBCPP_STD_VER >= 17
+    static_assert(is_constructible_v<__decay_t<_Fp>, _Fp>);
+    static_assert((is_constructible_v<__decay_t<_Args>, _Args> && ...));
+    static_assert(is_invocable_v<__decay_t<_Fp>, __decay_t<_Args>...>);
+#    endif
+
     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..4260068af268f 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>
@@ -1847,6 +1848,12 @@ 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) {
+#      if _LIBCPP_STD_VER >= 17
+  static_assert(is_constructible_v<__decay_t<_Fp>, _Fp>);
+  static_assert((is_constructible_v<__decay_t<_Args>, _Args> && ...));
+  static_assert(is_invocable_v<__decay_t<_Fp>, __decay_t<_Args>...>);
+#      endif
+
   typedef __async_func<__decay_t<_Fp>, __decay_t<_Args>...> _BF;
   typedef typename _BF::_Rp _Rp;
 
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..6b686cd9a1747 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,19 @@ 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&) = default;
+        F(F&&)      = delete;
+
+        void operator()() {}
+      };
+
+      static_cast<void>(std::async(F{}));
+      // expected-error@*:* {{static assertion failed}}
+      // expected-error@*:* {{call to deleted constructor}}
+      // expected-error@*:* 1+ {{uses 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..363f7c14559a8 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&) {}
+  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..1b8b8138f8066
--- /dev/null
+++ b/libcxx/test/std/thread/thread.threads/thread.thread.class/thread.thread.constr/F.verify.cpp
@@ -0,0 +1,63 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+// UNSUPPORTED: c++03, c++11, c++14
+
+// <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&&)      = delete;
+
+      void operator()() {}
+    };
+
+    F f;
+    std::thread t(f);
+    // expected-error@*:* {{static assertion failed}}
+    // expected-error@*:* {{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&&)      = delete;
+    };
+
+    struct F {
+      void operator()(const Arg&) const {}
+    };
+
+    Arg arg;
+    std::thread t(F{}, arg);
+    // expected-error@*:* {{static assertion failed}}
+    // expected-error@*:* 0+ {{call to deleted constructor}}
+    // expected-error@*:* 0+ {{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@*:* {{no matching function for call to '__invoke'}}
+  }
+}

>From 913bcd57451d0c2ef824246db95f60fd329de279 Mon Sep 17 00:00:00 2001
From: mleleszi <mleleszi at icloud.com>
Date: Mon, 22 Dec 2025 12:13:48 +0100
Subject: [PATCH 2/3] Implement LWG4376

---
 libcxx/docs/Status/Cxx23Issues.csv            |  2 +-
 libcxx/include/__thread/jthread.h             | 12 +--
 libcxx/include/future                         | 11 +--
 .../futures/futures.async/async.pass.cpp      | 25 +++++
 .../futures/futures.async/async.verify.cpp    | 58 ++++++++++-
 .../thread.jthread/cons.func.token.pass.cpp   | 25 +++++
 .../std/thread/thread.jthread/cons.verify.cpp | 95 +++++++++++++++++++
 .../thread.thread.constr/F.pass.cpp           |  4 +-
 .../thread.thread.constr/F.verify.cpp         | 40 +++++++-
 9 files changed, 252 insertions(+), 20 deletions(-)
 create mode 100644 libcxx/test/std/thread/thread.jthread/cons.verify.cpp

diff --git a/libcxx/docs/Status/Cxx23Issues.csv b/libcxx/docs/Status/Cxx23Issues.csv
index 389e1ad254a74..ef48759172382 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|","","`#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 difference is overconstrained","2020-11 (Virtual)","|Complete|","14","`#104310 <https://github.com/llvm/llvm-project/issues/104310>`__",""
diff --git a/libcxx/include/__thread/jthread.h b/libcxx/include/__thread/jthread.h
index 481ffe296c271..632e33bcfd447 100644
--- a/libcxx/include/__thread/jthread.h
+++ b/libcxx/include/__thread/jthread.h
@@ -49,12 +49,7 @@ class jthread {
   _LIBCPP_HIDE_FROM_ABI explicit jthread(_Fun&& __fun, _Args&&... __args)
     requires(!std::is_same_v<remove_cvref_t<_Fun>, jthread>)
       : __stop_source_(),
-        __thread_(__init_thread(__stop_source_, std::forward<_Fun>(__fun), std::forward<_Args>(__args)...)) {
-    static_assert(is_constructible_v<decay_t<_Fun>, _Fun>);
-    static_assert((is_constructible_v<decay_t<_Args>, _Args> && ...));
-    static_assert(is_invocable_v<decay_t<_Fun>, decay_t<_Args>...> ||
-                  is_invocable_v<decay_t<_Fun>, stop_token, decay_t<_Args>...>);
-  }
+        __thread_(__init_thread(__stop_source_, std::forward<_Fun>(__fun), std::forward<_Args>(__args)...)) {}
 
   _LIBCPP_HIDE_FROM_ABI ~jthread() {
     if (joinable()) {
@@ -116,6 +111,11 @@ class jthread {
 private:
   template <class _Fun, class... _Args>
   _LIBCPP_HIDE_FROM_ABI static thread __init_thread(const stop_source& __ss, _Fun&& __fun, _Args&&... __args) {
+    static_assert(is_constructible_v<decay_t<_Fun>, _Fun>);
+    static_assert((is_constructible_v<decay_t<_Args>, _Args> && ...));
+    static_assert(is_invocable_v<decay_t<_Fun>, decay_t<_Args>...> ||
+                  is_invocable_v<decay_t<_Fun>, stop_token, decay_t<_Args>...>);
+
     if constexpr (is_invocable_v<decay_t<_Fun>, stop_token, decay_t<_Args>...>) {
       return thread(std::forward<_Fun>(__fun), __ss.get_token(), std::forward<_Args>(__args)...);
     } else {
diff --git a/libcxx/include/future b/libcxx/include/future
index 4260068af268f..91c870df5b07b 100644
--- a/libcxx/include/future
+++ b/libcxx/include/future
@@ -1829,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_)) {}
 
@@ -1861,8 +1862,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)
@@ -1871,8 +1871,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 6b686cd9a1747..3c39b1264f1fe 100644
--- a/libcxx/test/std/thread/futures/futures.async/async.verify.cpp
+++ b/libcxx/test/std/thread/futures/futures.async/async.verify.cpp
@@ -28,6 +28,22 @@ 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@*:* {{call to deleted constructor}}
+    }
+
     { // Mandates: is_constructible_v<decay_t<F>, F>
       struct F {
         F()         = default;
@@ -39,7 +55,47 @@ void f() {
 
       static_cast<void>(std::async(F{}));
       // expected-error@*:* {{static assertion failed}}
+      // expected-error@*:* {{no matching constructor for initialization}}
       // expected-error@*:* {{call to deleted constructor}}
-      // expected-error@*:* 1+ {{uses deleted function}}
+    }
+
+    { // 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@*:* {{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@*:* {{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..fe96b07e0bedc
--- /dev/null
+++ b/libcxx/test/std/thread/thread.jthread/cons.verify.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
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+
+// <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@*:* 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@*:* 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@*:* 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&) = default;
+      Arg(Arg&&)      = delete;
+    };
+
+    struct F {
+      void operator()(const Arg&) const {}
+    };
+
+    std::jthread t(F{}, Arg{});
+    // expected-error@*:* 2 {{static assertion failed}}
+    // expected-error@*:* 2 {{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@*:* {{no matching function for call to '__invoke'}}
+  }
+}
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 363f7c14559a8..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
@@ -106,8 +106,8 @@ class MoveOnly
 class CopyOnly {
 public:
   CopyOnly() {}
-  CopyOnly(const CopyOnly&) {}
-  CopyOnly(CopyOnly&&) = delete;
+  CopyOnly(const CopyOnly&) = default;
+  CopyOnly(CopyOnly&&)      = delete;
 
   void operator()(const CopyOnly&) const {}
 };
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
index 1b8b8138f8066..4055005ad6ca9 100644
--- 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
@@ -23,7 +23,7 @@ void test() {
     struct F {
       F()         = default;
       F(const F&) = delete;
-      F(F&&)      = delete;
+      F(F&&)      = default;
 
       void operator()() {}
     };
@@ -35,11 +35,26 @@ void test() {
     // 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@*:* {{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&&)      = delete;
+      Arg(Arg&&)      = default;
     };
 
     struct F {
@@ -49,8 +64,25 @@ void test() {
     Arg arg;
     std::thread t(F{}, arg);
     // expected-error@*:* {{static assertion failed}}
-    // expected-error@*:* 0+ {{call to deleted constructor}}
-    // expected-error@*:* 0+ {{no matching constructor for initialization}}
+    // expected-error@*:* {{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::thread t(F{}, Arg{});
+    // expected-error@*:* {{static assertion failed}}
+    // expected-error@*:* {{call to deleted constructor}}
+    // expected-error@*:* {{no matching constructor for initialization}}
   }
 
   { // Mandates: is_invocable_v<decay_t<F>, decay_t<Args>...>

>From d29242a8ce18e8605d2452036b31d8d8d59f723f Mon Sep 17 00:00:00 2001
From: mleleszi <mleleszi at icloud.com>
Date: Tue, 23 Dec 2025 13:46:05 +0100
Subject: [PATCH 3/3] Address comments

---
 libcxx/docs/Status/Cxx23Issues.csv                   |  2 +-
 libcxx/include/__thread/jthread.h                    | 12 ++++++------
 libcxx/include/__thread/thread.h                     |  8 +++-----
 .../test/std/thread/thread.jthread/cons.verify.cpp   |  4 ++--
 .../thread.thread.constr/F.verify.cpp                |  4 +---
 5 files changed, 13 insertions(+), 17 deletions(-)

diff --git a/libcxx/docs/Status/Cxx23Issues.csv b/libcxx/docs/Status/Cxx23Issues.csv
index ef48759172382..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)","|Complete|","","`#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 difference is overconstrained","2020-11 (Virtual)","|Complete|","14","`#104310 <https://github.com/llvm/llvm-project/issues/104310>`__",""
diff --git a/libcxx/include/__thread/jthread.h b/libcxx/include/__thread/jthread.h
index 632e33bcfd447..481ffe296c271 100644
--- a/libcxx/include/__thread/jthread.h
+++ b/libcxx/include/__thread/jthread.h
@@ -49,7 +49,12 @@ class jthread {
   _LIBCPP_HIDE_FROM_ABI explicit jthread(_Fun&& __fun, _Args&&... __args)
     requires(!std::is_same_v<remove_cvref_t<_Fun>, jthread>)
       : __stop_source_(),
-        __thread_(__init_thread(__stop_source_, std::forward<_Fun>(__fun), std::forward<_Args>(__args)...)) {}
+        __thread_(__init_thread(__stop_source_, std::forward<_Fun>(__fun), std::forward<_Args>(__args)...)) {
+    static_assert(is_constructible_v<decay_t<_Fun>, _Fun>);
+    static_assert((is_constructible_v<decay_t<_Args>, _Args> && ...));
+    static_assert(is_invocable_v<decay_t<_Fun>, decay_t<_Args>...> ||
+                  is_invocable_v<decay_t<_Fun>, stop_token, decay_t<_Args>...>);
+  }
 
   _LIBCPP_HIDE_FROM_ABI ~jthread() {
     if (joinable()) {
@@ -111,11 +116,6 @@ class jthread {
 private:
   template <class _Fun, class... _Args>
   _LIBCPP_HIDE_FROM_ABI static thread __init_thread(const stop_source& __ss, _Fun&& __fun, _Args&&... __args) {
-    static_assert(is_constructible_v<decay_t<_Fun>, _Fun>);
-    static_assert((is_constructible_v<decay_t<_Args>, _Args> && ...));
-    static_assert(is_invocable_v<decay_t<_Fun>, decay_t<_Args>...> ||
-                  is_invocable_v<decay_t<_Fun>, stop_token, decay_t<_Args>...>);
-
     if constexpr (is_invocable_v<decay_t<_Fun>, stop_token, decay_t<_Args>...>) {
       return thread(std::forward<_Fun>(__fun), __ss.get_token(), std::forward<_Args>(__args)...);
     } else {
diff --git a/libcxx/include/__thread/thread.h b/libcxx/include/__thread/thread.h
index 24b4caa7f0d2f..b2f51aa816c10 100644
--- a/libcxx/include/__thread/thread.h
+++ b/libcxx/include/__thread/thread.h
@@ -207,11 +207,9 @@ 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) {
-#    if _LIBCPP_STD_VER >= 17
-    static_assert(is_constructible_v<__decay_t<_Fp>, _Fp>);
-    static_assert((is_constructible_v<__decay_t<_Args>, _Args> && ...));
-    static_assert(is_invocable_v<__decay_t<_Fp>, __decay_t<_Args>...>);
-#    endif
+    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);
diff --git a/libcxx/test/std/thread/thread.jthread/cons.verify.cpp b/libcxx/test/std/thread/thread.jthread/cons.verify.cpp
index fe96b07e0bedc..5c8fb22ec4e67 100644
--- a/libcxx/test/std/thread/thread.jthread/cons.verify.cpp
+++ b/libcxx/test/std/thread/thread.jthread/cons.verify.cpp
@@ -64,7 +64,7 @@ void test() {
     Arg arg;
     std::jthread t(F{}, arg);
     // expected-error@*:* 2 {{static assertion failed}}
-    // expected-error@*:* 2 {{call to deleted constructor}}
+    // expected-error@*:* {{call to deleted constructor}}
     // expected-error@*:* {{no matching constructor for initialization}}
   }
 
@@ -81,7 +81,7 @@ void test() {
 
     std::jthread t(F{}, Arg{});
     // expected-error@*:* 2 {{static assertion failed}}
-    // expected-error@*:* 2 {{call to deleted constructor}}
+    // expected-error@*:* {{call to deleted constructor}}
     // expected-error@*:* {{no matching constructor for initialization}}
   }
 
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
index 4055005ad6ca9..8970f8c024290 100644
--- 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
@@ -7,7 +7,7 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: no-threads
-// UNSUPPORTED: c++03, c++11, c++14
+// UNSUPPORTED: c++03
 
 // <thread>
 
@@ -64,7 +64,6 @@ void test() {
     Arg arg;
     std::thread t(F{}, arg);
     // expected-error@*:* {{static assertion failed}}
-    // expected-error@*:* {{call to deleted constructor}}
     // expected-error@*:* {{no matching constructor for initialization}}
   }
 
@@ -81,7 +80,6 @@ void test() {
 
     std::thread t(F{}, Arg{});
     // expected-error@*:* {{static assertion failed}}
-    // expected-error@*:* {{call to deleted constructor}}
     // expected-error@*:* {{no matching constructor for initialization}}
   }
 



More information about the libcxx-commits mailing list