[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