P3771R0: constexpr mutex, locks, and condition variable
Motivation
It's really hard to conditionally avoid non-constexpr
types in a code which is supposed to be constexpr
compatible. This paper fixes it by making these types (and algorithms) constexpr
compatible. There is no semantical change for updated types and algorithms.
This paper is a continuation of paper P3309R3: constexpr atomic & atomic_ref
and makes a lot of library code reusable in constexpr
world.
Reusability
Main objective is being able to reuse same code in any environment (runtime, GPU, and now also constant evaluated). This makes C++ programs much easier to read and write and more bug-prone as someone wise once said "for every line there is a bug".
Also it lowers cognitive burden when users don't need to care what is and what's not constexpr
compatible.
Example with std::mutex
Type std::mutex
has constexpr
default constructor since C++11, but std::lock_guard
doesn't. You can't just wrap the auto _ = std::lock_guard{mtx}
into if consteval
.
template <typename T> class locked_queue {
std::queue<T> impl{};
std::mutex mtx{};
public:
locked_queue() = default;
constexpr void push(T input) {
auto _ = std::lock_guard{mtx}; // BEFORE: unable to call non-constexpr constructor
// AFTER: fine
impl.push(std::move(input));
}
constexpr std::optional<T> pop() {
auto _ = std::lock_guard{mtx}; // BEFORE: unable to call non-constexpr constructor
// AFTER: fine
if (impl.empty()) {
return std::nullopt;
}
auto r = std::move(impl.front());
impl.pop();
return r;
}
}
consteval foo my_algorithm() {
auto queue = locked_queue<int>{};
queue.push(1); // BEFORE: unable to call non-constexpr constructor of `lock_guard`
queue.push(42); //
queue.push(11); //
// AFTER: I can reuse my code!
return process(queue);
}
One possibility to make this work without this paper would look like this:
template <typename T> class locked_queue {
std::queue<T> impl{};
std::mutex mtx{};
constexpr void unsync_push(T input) {
impl.push(std::move(input));
}
constexpr std::optional<T> unsync_pop() {
if (impl.empty()) {
return std::nullopt;
}
auto r = std::move(impl.front());
impl.pop();
return r;
}
public:
constexpr void push(T input) {
if consteval {
unsafe_push(std::move(input));
} else {
auto _ = std::lock_guard{mtx};
unsafe_push(std::move(input));
}
}
constexpr std::optional<T> pop() {
if consteval {
return unsafe_pop();
} else {
auto _ = std::lock_guard{mtx};
return unsafe_pop();
}
}
}
This pattern is terrible and it creates opportunities for more bugs, and it makes testing harder (especially when test coverage mapping is used).
Example with std::shared_mutex
template <typename T> class protected_object {
std::optional<T> object{std::nullopt};
std::shared_mutex mtx{}; // BEFORE: unable to use non-constexpr constructor
// AFTER: fine
public:
template <typename... Args> constexpr void set(Args && ... args) {
auto _ = std::unique_lock{mtx};
object.emplace(std::forward<Args>(args)...);
}
std::optional<T;> get() const {
auto _ = std::shared_lock{mtx};
return object;
}
}
Type std::shared_mutex
doesn't have constexpr
default constructor. There is not a simple solution how to avoid the error.
No deadlocks
A deadlock is undefined behaviour (missing citation). Any undefined behaviour during constant evaluation is making program ill-formed [expr.const].
Change
In terms of specification it's just adding constexpr
everywhere in section thread.mutex (including thread.lock and thread.once) and thread.condition. Semantic of all types and algorithms is clearly defined and is non-surprising to users.
One optional additional wording change is making sure no synchronization primitive can leave constant evaluation in non-default state (which will be useful also in future for semaphors
).
Quesion about timed locking
Types timed_mutex
, shared_timed_mutex
, recursive_timed_mutex
, and some methods on unique_lock
and shared_lock
have functionality which allows to give up and not take the ownership of lock after certain time or at specific timepoint. There is not observable time during constant evaluation, there are three possible options:
- simply not make
try_lock_for
nortry_lock_until
functionalityconstexpr
(and forcing users toif consteval
such code away, which is against motivation of this paper), - automatically fail to take ownership if already locked (author's preferred, with wording),
- make only
try_lock_for
constexpr
, and not maketry_lock_until
, and block compilation for specified duration.
I prefer option with quick failure to take the ownership, as we know in single-threaded environment we would block for some time and fail anyway. And there is no way how to observe time during constant evaluation anyway. You can think about this as fast forward of time.
Native handles
Functions returning native handles are not marked constexpr
as these are an escape hatch to use platform specific code.
Utility functions
There is function notify_all_at_thread_exit
which is marked constexpr
, in single threaded environment is a no-op and it's trivial to implement it that way. If we won't implement it, we will force users to write if consteval
everytime they use it, and that's not a good user experience.
This paper also proposes making constexpr
free functions implementing interruptable waits, and we can do so as stop_token
is already constexpr
default constructible thanks to making shared_ptr
constexpr in P3037R6. Because there is no other thread which can interrupt the wait, these function will behave similarly as non-interruptable wait functions (meaning fail immediately to obtain lock due the new paragraph in [thread.req.timing]).
Implementation
I started working on the implementation in libc++, following subsections contains notes from my research how all three major standard library implements impacted types from this paper.
libc++
Type mutex
and other mutex-based types has definition of methods in a source file in which these methods are abstract into low-level __libcpp_mutex_*
functions, which are defined in their platform specific header files (dispatched here, C11, POSIX, win32 with its implementation). Support to constexpr
mutex default constructor is done thru providing constant _LIBCPP_MUTEX_INITIALIZER
which is defined as {}
(for C11) or PTHREAD_MUTEX_INITIALIZER
(for POSIX threads). This tells me same thing (creating _LIBCPP_*_INITIALIZER
macros) can be done for other mutex types, as this is already supported by posix threads and can be done also with win32.
Type condition_variable
default constructor is surprisingly already constexpr (so much about the requirement in [constexpr.function]). Main functionality is in a source file where it is using already abstracted away functions similarly as mutex. These few files will need to be moved to header file too.
why is it in source files
Based on my experience implementing constexpr
exceptions I have noticed libc++ tends to hide the exception throwing code in to source files in shared library. This code mostly will be needed to moved to header files anyway due the constexpr
exception support. The two layer of abstraction for mutex
and condition_variable
won't be needed anymore after that.
shared_mutex
implementation detail in libc++
This is code from libc++ with removed annotational macros. Unfortunetely the __shared_mutex_base
contains methods defined in a .cpp
file. But internal mutex
and condition_variable
types are already default constexpr constructible and the __shared_mutex_base
constructor only sets __state_
to zero, so this constructor can be easily made constexpr
.
All the single-threaded semantic can be then put into shared_mutex
type, with if consteval
and compiler builtin to attach constant evaluation metadata to an object.
struct __shared_mutex_base {
mutex __mut_;
condition_variable __gate1_;
condition_variable __gate2_;
unsigned __state_{0};
static const unsigned __write_entered_ = 1U << (sizeof(unsigned) * __CHAR_BIT__ - 1);
static const unsigned __n_readers_ = ~__write_entered_;
__shared_mutex_base() = default;
~__shared_mutex_base() = default;
__shared_mutex_base(const __shared_mutex_base&) = delete;
__shared_mutex_base& operator=(const __shared_mutex_base&) = delete;
// Exclusive ownership
void lock(); // blocking
bool try_lock();
void unlock();
// Shared ownership
void lock_shared(); // blocking
bool try_lock_shared();
void unlock_shared();
};
class shared_mutex {
__shared_mutex_base __base_;
public:
constexpr shared_mutex() : __base_() {}
~shared_mutex() = default;
shared_mutex(const shared_mutex&) = delete;
shared_mutex& operator=(const shared_mutex&) = delete;
// Exclusive ownership
constexpr void lock() {
if consteval {
return __builtin_metadata_unique_lock(&__base_);
if (__base_.state != 0) std::abort;
__base_.state = 1;
} else {
return __base_.lock();
}
}
constexpr bool try_lock() {
if consteval {
return __builtin_metadata_try_unique_lock(&__base_);
if (__base_.state != 0) return false;
__base_.state = 1;
return true;
} else {
return __base_.try_lock();
}
}
constexpr void unlock() {
if consteval {
return __builtin_metadata_unique_unlock(&__base_);
if (__base_.state != 1) std::abort();
__base_.state = 0;
} else {
return __base_.unlock();
}
}
// Shared ownership
constexpr void lock_shared() {
if consteval {
return __builtin_metadata_shared_lock(&__base_);
if (__base_.state == 0) __base_.state = 2;
else if (__base_.state == 1) std::abort();
else ++__base_.state;
} else {
return __base_.lock_shared();
}
}
constexpr bool try_lock_shared() {
if consteval {
return __builtin_metadata_try_shared_lock(&__base_);
if (__base_.state == 0) __base_.state = 2;
else if (__base_.state == 1) return false;
else ++__base_.state;
return true;
} else {
return __base_.try_lock_shared();
}
}
constexpr void unlock_shared() {
if consteval {
return __builtin_metadata_shared_unlock(&__base_);
if (__base_.state == 0) std::abort();
else if (__base_.state == 1) std::abort();
else if (__base_.state == 2) __base_.state = 0;
else --__base_.state;
} else {
return __base_.unlock_shared();
}
}
};
Purposes of these builtins is to provided associated metadata to the object itself, and use them for useful diagnostics.
Alternative approach would be use __shared_mutex_base::__state_
for it, and use library functionality to provide error messages in case of deadlock, but this approach doesn't allow us easily to diagnose where the lock was previously obtained.
Research of libstdc++
Libstdc++ is supporting many platforms and code around synchronization primitives is somehow bit convoluted due macro abstractions and #ifdef
-s rules to select appropriate implementation.
std::mutex
Most basic mutex type is all defined in headers it's default constructor is defaulted or explicitly made constexpr in presence of macro __GTHREAD_MUTEX_INIT
. Native type is hidden in __mutex_base
which in order to support constexpr default initialize the __gthread_mutex_t
native handle.
std::shared_mutex
Shared mutex type's default constructor is not historically marked constexpr
. It's also defined all in a header file. Construction of native handle is abstract in __shared_mutex_pthread
or __shared_mutex_cv
which is used when platform doesn't provide _GLIBCXX_USE_PTHREAD_RWLOCK_T
and its implementation is normal mutex and two condition variables, similarly as in libc++.
In case pthread of the platform provides RW lock, the type __shared_mutex_pthread
abstracts it away. If macro PTHREAD_RWLOCK_INITIALIZER
is available, it's used to defualt initialize the lock. This macro is on most major platform providing aggregate or numeric constant initialization (macOS, linux, win32's pthreads).
lock guards
All lock guards types (lock_guard
, unique_lock
, shared_lock
, scoped_lock
) are just RAII wrappers over interfaces of mutexes. There is nothing special and nothing which interfere with making them constexpr, these are already defined in header files.
std::condition_variable
Condition variable types are defined partially in header file. Implementation of some function is in a source file and they are not doing anything platform specific. Type __condvar
which abstracts native handle is defined next to std::mutex. It doesn't contain same abstraction as was done for the mutex type, even when most pthread libraries provides equivalent macro PTHREAD_COND_INITIALIZER
for constant initialization. This piece of code would benefit from update. Making __condvar
constexpr
compatible would be then trivial.
MS STL
mutex
, recursive_mutex
, and timed_mutex
all share _Mutex_base
and making these types constexpr
will be trivial with if consteval
. Same applies to shared_mutex
too.
Type condition_variable
is defined in header, but it uses implementation function defined in source files. These can be circumvent easily with if consteval
. There is a _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR
macro, which optionaly disables std::mutex
's constructor and it makes the condition variable initialized with function defined outside. This is a deviation from the standard, and when this special macro is not set there is nothing which prevents condition variable to be constexpr
compatible.
Wording
Modify constant evaluation
Purpose of this change is to disallow creation of already locked synchronization objects and their subsequent leakage into runtime code.
7.7 Constant expressions [expr.const]
- this ([expr.prim.this]), except
- in a constexpr function ([dcl.constexpr]) that is being evaluated as part of E or
- when appearing as the postfix-expression of an implicit or explicit class member access expression ([expr.ref]);
- a control flow that passes through
a declaration of a block variable ([basic.scope.block]) with
static ([basic.stc.static]) or
thread ([basic.stc.thread]) storage duration,
unless that variable is usable in constant expressions;
[Example 4: constexpr char test() { static const int x = 5; static constexpr char c[] = "Hello World"; return *(c + x); } static_assert(' ' == test()); — end example]
- an invocation of a non-constexpr function;67
- an invocation of an undefined constexpr function;
- an invocation of an instantiated constexpr function that is not constexpr-suitable;
- an invocation of a virtual function ([class.virtual]) for an object whose dynamic type is constexpr-unknown;
- an expression that would exceed the implementation-defined limits (see [implimits]);
- an operation that would have undefined or erroneous behavior as specified in [intro] through [cpp];68
- an lvalue-to-rvalue conversion unless
it is applied to
- a glvalue of type cv std::nullptr_t,
- a non-volatile glvalue that refers to an object that is usable in constant expressions, or
- a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of E;
- an lvalue-to-rvalue conversion that is applied to a glvalue that refers to a non-active member of a union or a subobject thereof;
- an lvalue-to-rvalue conversion that is applied to an object with an indeterminate value;
- an invocation of an implicitly-defined copy/move constructor or copy/move assignment operator for a union whose active member (if any) is mutable, unless the lifetime of the union object began within the evaluation of E;
- in a lambda-expression,
a reference to this or to a variable with
automatic storage duration defined outside that
lambda-expression, where
the reference would be an odr-use ([basic.def.odr], [expr.prim.lambda]);
[Example 5: void g() { const int n = 0; [=] { constexpr int i = n; // OK, n is not odr-used here constexpr int j = *&n; // error: &n would be an odr-use of n }; } — end example][Note 4:If the odr-use occurs in an invocation of a function call operator of a closure type, it no longer refers to this or to an enclosing variable with automatic storage duration due to the transformation ([expr.prim.lambda.capture]) of the id-expression into an access of the corresponding data member.— end note][Example 6: auto monad = [](auto v) { return [=] { return v; }; }; auto bind = [](auto m) { return [=](auto fvm) { return fvm(m()); }; }; // OK to capture objects with automatic storage duration created during constant expression evaluation. static_assert(bind(monad(2))(monad)() == monad(2)()); — end example]
- a conversion from a prvalue P of type “pointer to cv void” to a type “cv1 pointer to T”, where T is not cv2 void, unless P is a null pointer value or points to an object whose type is similar to T;
- a reinterpret_cast ([expr.reinterpret.cast]);
- a modification of an object ([expr.assign], [expr.post.incr], [expr.pre.incr]) unless it is applied to a non-volatile lvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of E;
- an invocation of a destructor ([class.dtor]) or a function call whose postfix-expression names a pseudo-destructor ([expr.call]), in either case for an object whose lifetime did not begin within the evaluation of E;
- a new-expression ([expr.new]),
unless either
- the selected allocation function is a replaceable global allocation function ([new.delete.single], [new.delete.array]) and the allocated storage is deallocated within the evaluation of E, or
- the selected allocation function is
a non-allocating form ([new.delete.placement])
with an allocated type T, where
- the placement argument to the new-expression points to an object whose type is similar to T ([conv.qual]) or, if T is an array type, to the first element of an object of a type similar to T, and
- the placement argument points to storage whose duration began within the evaluation of E;
- a delete-expression ([expr.delete]), unless it deallocates a region of storage allocated within the evaluation of E;
- a call to an instance of std::allocator<T>::allocate ([allocator.members]), unless the allocated storage is deallocated within the evaluation of E;
- a call to an instance of std::allocator<T>::deallocate ([allocator.members]), unless it deallocates a region of storage allocated within the evaluation of E;
- a construction of an exception object, unless the exception object and all of its implicit copies created by invocations of std::current_exception or std::rethrow_exception ([propagation]) are destroyed within the evaluation of E;
- a construction of a synchronization object [thread], unless the object is destroyed within the evaluation of E or is in its default state when evaluation of E is finished;
- an await-expression ([expr.await]);
- a yield-expression ([expr.yield]);
- a three-way comparison ([expr.spaceship]), relational ([expr.rel]), or equality ([expr.eq]) operator where the result is unspecified;
- a dynamic_cast ([expr.dynamic.cast]) or typeid ([expr.typeid]) expression on a glvalue that refers to an object whose dynamic type is constexpr-unknown;
- a dynamic_cast ([expr.dynamic.cast]) expression, typeid ([expr.typeid]) expression, or new-expression ([expr.new]) that would throw an exception where no definition of the exception type is reachable;
- an asm-declaration ([dcl.asm]);
- an invocation of the va_arg macro ([cstdarg.syn]);
- a non-constant library call ([defns.nonconst.libcall]); or
- a goto statement ([stmt.goto]). [Note 5: — end note]
Mutexes and locking
32.6 Mutual exclusion [thread.mutex]
32.6.1 General [thread.mutex.general]
32.6.2 Header <mutex> synopsis [mutex.syn]
32.6.4 Mutex requirements [thread.mutex.requirements]
32.6.4.1 General [thread.mutex.requirements.general]
32.6.4.2 Mutex types [thread.mutex.requirements.mutex]
32.6.4.2.1 General [thread.mutex.requirements.mutex.general]
32.6.4.2.2 Class mutex [thread.mutex.class]
32.6.4.2.3 Class recursive_mutex [thread.mutex.recursive]
32.6.4.3 Timed mutex types [thread.timedmutex.requirements]
32.6.4.3.1 General [thread.timedmutex.requirements.general]
32.6.4.3.2 Class timed_mutex [thread.timedmutex.class]
32.6.4.3.3 Class recursive_timed_mutex [thread.timedmutex.recursive]
32.6.5 Locks [thread.lock]
32.6.5.1 General [thread.lock.general]
32.6.5.2 Class template lock_guard [thread.lock.guard]
constexpr explicit lock_guard(mutex_type& m);
constexpr lock_guard(mutex_type& m, adopt_lock_t);
constexpr ~lock_guard();
32.6.5.3 Class template scoped_lock [thread.lock.scoped]
- Otherwise, all types in the template parameter pack MutexTypes shall meet the Cpp17Lockable requirements ([thread.req.lockable.req]) and there is no member mutex_type.
constexpr explicit scoped_lock(MutexTypes&... m);
constexpr explicit scoped_lock(adopt_lock_t, MutexTypes&... m);
constexpr ~scoped_lock();
32.6.5.4 Class template unique_lock [thread.lock.unique]
32.6.5.4.1 General [thread.lock.unique.general]
32.6.5.4.2 Constructors, destructor, and assignment [thread.lock.unique.cons]
constexpr unique_lock() noexcept;
constexpr explicit unique_lock(mutex_type& m);
constexpr unique_lock(mutex_type& m, defer_lock_t) noexcept;
constexpr unique_lock(mutex_type& m, try_to_lock_t);
constexpr unique_lock(mutex_type& m, adopt_lock_t);
template<class Clock, class Duration>
constexpr unique_lock(mutex_type& m, const chrono::time_point<Clock, Duration>& abs_time);
template<class Rep, class Period>
constexpr unique_lock(mutex_type& m, const chrono::duration<Rep, Period>& rel_time);
constexpr unique_lock(unique_lock&& u) noexcept;
constexpr unique_lock& operator=(unique_lock&& u) noexcept;
constexpr ~unique_lock();
32.6.5.4.3 Locking [thread.lock.unique.locking]
constexpr void lock();
constexpr bool try_lock();
template<class Clock, class Duration>
constexpr bool try_lock_until(const chrono::time_point<Clock, Duration>& abs_time);
template<class Rep, class Period>
constexpr bool try_lock_for(const chrono::duration<Rep, Period>& rel_time);
constexpr void unlock();
32.6.5.4.4 Modifiers [thread.lock.unique.mod]
constexpr void swap(unique_lock& u) noexcept;
constexpr mutex_type* release() noexcept;
template<class Mutex>
constexpr void swap(unique_lock<Mutex>& x, unique_lock<Mutex>& y) noexcept;
32.6.6 Generic locking algorithms [thread.lock.algorithm]
template<class L1, class L2, class... L3> constexpr int try_lock(L1&, L2&, L3&...);
template<class L1, class L2, class... L3> constexpr void lock(L1&, L2&, L3&...);
32.6.7 Call once [thread.once]
32.6.7.1 Struct once_flag [thread.once.onceflag]
constexpr once_flag() noexcept;
32.6.7.2 Function call_once [thread.once.callonce]
template<class Callable, class... Args>
constexpr void call_once(once_flag& flag, Callable&& func, Args&&... args);
Timing specifications
32.2.4 Timing specifications [thread.req.timing]
- If , the waiting function should not time out until Clock::now() returns a time , i.e., waking at .[Note 1:When the clock is adjusted backwards, this specification can result in the total duration of the wait increasing when measured against a steady clock.When the clock is adjusted forwards, this specification can result in the total duration of the wait decreasing when measured against a steady clock.— end note]
Condition variables
32.7 Condition variables [thread.condition]
32.7.1 General [thread.condition.general]
32.7.2 Header <condition_variable> synopsis [condition.variable.syn]
32.7.3 Non-member functions [thread.condition.nonmember]
constexpr void notify_all_at_thread_exit(condition_variable& cond, unique_lock<mutex> lk);
32.7.4 Class condition_variable [thread.condition.condvar]
constexpr condition_variable();
constexpr ~condition_variable();
constexpr void notify_one() noexcept;
constexpr void notify_all() noexcept;
constexpr void wait(unique_lock<mutex>& lock);
- no other thread is waiting on this condition_variable object or
- lock.mutex() returns the same value for each of the lock arguments supplied by all concurrently waiting (via wait, wait_for, or wait_until) threads.
template<class Predicate>
constexpr void wait(unique_lock<mutex>& lock, Predicate pred);
- no other thread is waiting on this condition_variable object or
- lock.mutex() returns the same value for each of the lock arguments supplied by all concurrently waiting (via wait, wait_for, or wait_until) threads.
template<class Clock, class Duration>
constexpr cv_status wait_until(unique_lock<mutex>& lock,
const chrono::time_point<Clock, Duration>& abs_time);
- no other thread is waiting on this condition_variable object or
- lock.mutex() returns the same value for each of the lock arguments supplied by all concurrently waiting (via wait, wait_for, or wait_until) threads.
- When unblocked, calls lock.lock() (possibly blocking on the lock), then returns.
- The function will unblock when signaled by a call to notify_one(), a call to notify_all(), expiration of the absolute timeout ([thread.req.timing]) specified by abs_time, or spuriously.
- If the function exits via an exception, lock.lock() is called prior to exiting the function.
template<class Rep, class Period>
constexpr cv_status wait_for(unique_lock<mutex>& lock,
const chrono::duration<Rep, Period>& rel_time);
- no other thread is waiting on this condition_variable object or
- lock.mutex() returns the same value for each of the lock arguments supplied by all concurrently waiting (via wait, wait_for, or wait_until) threads.
template<class Clock, class Duration, class Predicate>
constexpr bool wait_until(unique_lock<mutex>& lock,
const chrono::time_point<Clock, Duration>& abs_time,
Predicate pred);
- no other thread is waiting on this condition_variable object or
- lock.mutex() returns the same value for each of the lock arguments supplied by all concurrently waiting (via wait, wait_for, or wait_until) threads.
template<class Rep, class Period, class Predicate>
constexpr bool wait_for(unique_lock<mutex>& lock,
const chrono::duration<Rep, Period>& rel_time,
Predicate pred);
- no other thread is waiting on this condition_variable object or
- lock.mutex() returns the same value for each of the lock arguments supplied by all concurrently waiting (via wait, wait_for, or wait_until) threads.
32.7.5 Class condition_variable_any [thread.condition.condvarany]
32.7.5.1 General [thread.condition.condvarany.general]
constexpr condition_variable_any();
constexpr ~condition_variable_any();
constexpr void notify_one() noexcept;
constexpr void notify_all() noexcept;
32.7.5.2 Noninterruptible waits [thread.condvarany.wait]
template<class Lock>
constexpr void wait(Lock& lock);
template<class Lock, class Predicate>
constexpr void wait(Lock& lock, Predicate pred);
template<class Lock, class Clock, class Duration>
constexpr cv_status wait_until(Lock& lock, const chrono::time_point<Clock, Duration>& abs_time);
- When unblocked, calls lock.lock() (possibly blocking on the lock) and returns.
- The function will unblock when signaled by a call to notify_one(), a call to notify_all(), expiration of the absolute timeout ([thread.req.timing]) specified by abs_time, or spuriously.
- If the function exits via an exception, lock.lock() is called prior to exiting the function.
template<class Lock, class Rep, class Period>
constexpr cv_status wait_for(Lock& lock, const chrono::duration<Rep, Period>& rel_time);
template<class Lock, class Clock, class Duration, class Predicate>
constexpr bool wait_until(Lock& lock, const chrono::time_point<Clock, Duration>& abs_time, Predicate pred);
template<class Lock, class Rep, class Period, class Predicate>
constexpr bool wait_for(Lock& lock, const chrono::duration<Rep, Period>& rel_time, Predicate pred);
32.7.5.3 Interruptible waits [thread.condvarany.intwait]
template<class Lock, class Predicate>
constexpr bool wait(Lock& lock, stop_token stoken, Predicate pred);
template<class Lock, class Clock, class Duration, class Predicate>
constexpr bool wait_until(Lock& lock, stop_token stoken,
const chrono::time_point<Clock, Duration>& abs_time, Predicate pred);
template<class Lock, class Rep, class Period, class Predicate>
constexpr bool wait_for(Lock& lock, stop_token stoken,
const chrono::duration<Rep, Period>& rel_time, Predicate pred);
Feature test macro
15.11 Predefined macro names [cpp.predefined]
__cpp_constexpr_synchronization 20????L
17.3.2 Header <version> synopsis [version.syn]
#define __cpp_lib_constexpr_mutex 20????L // also in <mutex>
#define __cpp_lib_constexpr_recursive_mutex 20????L // also in <mutex>
#define __cpp_lib_constexpr_timed_mutex 20????L // also in <mutex>
#define __cpp_lib_constexpr_timed_recursive_mutex 20????L // also in <mutex>
#define __cpp_lib_constexpr_shared_mutex 20????L // also in <shared_mutex>
#define __cpp_lib_constexpr_shared_timed_mutex 20????L // also in <shared_mutex>
#define __cpp_lib_constexpr_lock_guard 20????L // also in <mutex>
#define __cpp_lib_constexpr_scoped_lock 20????L // also in <mutex>
#define __cpp_lib_constexpr_unique_lock 20????L // also in <mutex>
#define __cpp_lib_constexpr_shared_lock 20????L // also in <mutex>
#define __cpp_lib_constexpr_call_once 20????L // also in <mutex>
#define __cpp_lib_constexpr_locking_algorithms 20????L // also in <mutex>
#define __cpp_lib_constexpr_condition_variable 20????L // also in <condition_variable>
#define __cpp_lib_constexpr_condition_variable_any 20????L // also in <condition_variable>