This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of New status.

4301. condition_variable{_any}::wait_{for, until} should take timeout by value

Section: 32.7.4 [thread.condition.condvar], 32.7.5 [thread.condition.condvarany] Status: New Submitter: Hui Xie Opened: 2025-07-19 Last modified: 2025-07-26

Priority: Not Prioritized

View other active issues in [thread.condition.condvar].

View all other issues in [thread.condition.condvar].

View all issues with New status.

Discussion:

At the moment, both condition_variable and condition_variable_any's wait_for and wait_until take the timeout time_point/duration by const reference. This can cause surprising behaviour. Given the following example (thanks to Tim Song):

struct Task {
  system_clock::time_point deadline;
  // stuff
};

std::mutex mtx;
std::condition_variable cv;
std::priority_queue<Task, vector<Task>, CompareDeadlines> queue;

// thread 1
std::unique_lock lck(mtx);
if (queue.empty()) { cv.wait(lck); }
else { cv.wait_until(lck, queue.top().deadline); }

// thread 2
std::lock_guard lck(mtx);
queue.push(/* some task */);
cv.notify_one();

From the user's point of view, it is sufficiently locked on both threads. However, due to the fact that the time_point is taken by reference, and that both libc++ and libstdc++'s implementation will read the value again after waking up, this will read a dangling reference of the time_point.

Another example related to this issue:

We (libc++) recently received a bug report on condition_variable{_any}::wait_{for, until}.

Basically the user claims that these functions take time_point/duration by const reference, if the user modifies the time_point/duration on another thread with the same mutex, they can get unexpected return value for condition_variable, and data race for conditional_variable_any.

Bug report here.

Reproducer (libstdc++ has the same behaviour as ours) on godbolt.

std::mutex mutex;
std::condition_variable cv;
auto timeout = std::chrono::steady_clock::time_point::max();

// Thread 1:
std::unique_lock lock(mutex);
const auto status = cv.wait_until(lock, timeout);

// Thread 2:
std::unique_lock lock(mutex);
cv.notify_one();
timeout = std::chrono::steady_clock::time_point::min();

So basically the problem was that when we return whether there is no_timeout or timeout at the end of the function, we read the const reference again, which can be changed since the beginning of the function. For condition_variable, it is "unexpected results" according to the user. And in conditional_variable_any, we actually unlock the user lock and acquire our internal lock, then read the input again, so this is actually a data race.

For wait_for, the spec has

Effects: Equivalent to: return wait_until(lock, chrono::steady_clock::now() + rel_time);

So the user can claim our implementation is not conforming because the spec says there needs to be a temporary time_point (now + duration) created and since it should operate on this temporary time_point. There shouldn't be any unexpected behaviour or data race .

For wait_until it is unclear whether the spec has implications that implementations are allowed to read abs_time while the user's lock is unlocked.

it is also unclear if an implementation is allowed to return timeout if cv indeed does not wait longer than the original value of timeout. If it is not allowed, implementations will have to make a local copy of the input rel_time or abs_time, which defeats the purpose of taking arguments by const reference.

For both of the examples, Ville has a great comment in the reflector:

It seems like a whole bag of problems goes away if these functions just take the timeout by value?

libc++ implementers have strong preference just changing the API to take these arguments by value, and it is not an ABI break for us as the function signature has changed.

Proposed resolution:

This wording is relative to this CD preview draft.

  1. Modify 32.7.4 [thread.condition.condvar] as indicated:

    namespace std {
      class condition_variable {
      public:
        […]
        template<class Predicate>
          void wait(unique_lock<mutex>& lock, Predicate pred);
        template<class Clock, class Duration>
          cv_status wait_until(unique_lock<mutex>& lock,
                               const chrono::time_point<Clock, Duration>& abs_time);
        template<class Clock, class Duration, class Predicate>
          bool wait_until(unique_lock<mutex>& lock,
                          const chrono::time_point<Clock, Duration>& abs_time,
                          Predicate pred);
        template<class Rep, class Period>
          cv_status wait_for(unique_lock<mutex>& lock,
                             const chrono::duration<Rep, Period>& rel_time);
        template<class Rep, class Period, class Predicate>
          bool wait_for(unique_lock<mutex>& lock,
                        const chrono::duration<Rep, Period>& rel_time,
                        Predicate pred);    
        […]
      };
    }
    
    […]
    template<class Clock, class Duration>
      cv_status wait_until(unique_lock<mutex>& lock,
                           const chrono::time_point<Clock, Duration>& abs_time);
    

    -17- Preconditions: […]

    […]

    template<class Rep, class Period>
      cv_status wait_for(unique_lock<mutex>& lock,
                         const chrono::duration<Rep, Period>& rel_time);
    

    -23- Preconditions: […]

    […]

    template<class Clock, class Duration, class Predicate>
      bool wait_until(unique_lock<mutex>& lock,
                      const chrono::time_point<Clock, Duration>& abs_time,
                      Predicate pred);
    

    -29- Preconditions: […]

    […]

    template<class Rep, class Period, class Predicate>
      bool wait_for(unique_lock<mutex>& lock,
                    const chrono::duration<Rep, Period>& rel_time,
                    Predicate pred);    
    

    -35- Preconditions: […]

    […]

  2. Modify 32.7.5.1 [thread.condition.condvarany.general] as indicated:

    namespace std {
      class condition_variable_any {
      public:
        […]
        // 32.7.5.2 [thread.condvarany.wait], noninterruptible waits
        template<class Lock>
          void wait(Lock& lock);
        template<class Lock, class Predicate>
          void wait(Lock& lock, Predicate pred);
        
        template<class Lock, class Clock, class Duration>
          cv_status wait_until(Lock& lock, const chrono::time_point<Clock, Duration>& abs_time);
        template<class Lock, class Clock, class Duration, class Predicate>
          bool wait_until(Lock& lock, const chrono::time_point<Clock, Duration>& abs_time,
                          Predicate pred);
        template<class Lock, class Rep, class Period>
          cv_status wait_for(Lock& lock, const chrono::duration<Rep, Period>& rel_time);
        template<class Lock, class Rep, class Period, class Predicate>
          bool wait_for(Lock& lock, const chrono::duration<Rep, Period>& rel_time, Predicate pred);
       
        // 32.7.5.3 [thread.condvarany.intwait], interruptible waits
        template<class Lock, class Predicate>
          bool wait(Lock& lock, stop_token stoken, Predicate pred);
        template<class Lock, class Clock, class Duration, class Predicate>
          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>
          bool wait_for(Lock& lock, stop_token stoken,
                        const chrono::duration<Rep, Period>& rel_time, Predicate pred);
      };
    }
    
  3. Modify 32.7.5.2 [thread.condvarany.wait] as indicated:

    […]
    template<class Lock, class Clock, class Duration>
      cv_status wait_until(Lock& lock, const chrono::time_point<Clock, Duration>& abs_time);
    

    -6- Effects: […]

    […]

    template<class Lock, class Rep, class Period>
      cv_status wait_for(Lock& lock, const chrono::duration<Rep, Period>& rel_time);
    

    -11- Effects: […]

    […]

    template<class Lock, class Clock, class Duration, class Predicate>
      bool wait_until(Lock& lock, const chrono::time_point<Clock, Duration>& abs_time,
                      Predicate pred);
    

    -16- Effects: […]

    […]

    template<class Lock, class Rep, class Period, class Predicate>
      bool wait_for(Lock& lock, const chrono::duration<Rep, Period>& rel_time, Predicate pred);
    

    -19- Effects: […]

  4. Modify 32.7.5.3 [thread.condvarany.intwait] as indicated:

    […]
    template<class Lock, class Clock, class Duration, class Predicate>
      bool wait_until(Lock& lock, stop_token stoken,
                      const chrono::time_point<Clock, Duration>& abs_time, Predicate pred);
    

    -7- Effects: […]

    […]

    template<class Lock, class Rep, class Period, class Predicate>
      bool wait_for(Lock& lock, stop_token stoken,
                    const chrono::duration<Rep, Period>& rel_time, Predicate pred);
    

    -13- Effects: […]