пятница, 1 августа 2025 г.

[prog.c++.bugs] Пример типичной C++ной ошибки

Давеча потратил некоторое время дабы найти и устранить элементарный баг. Причем самое веселое было то, что сперва я заставил код работать с отладочными печатями устранив все косяки, допущенные при реализации алгоритма. Потом убрал все отладочные печати и полезли ошибки. Вернул часть печатей -- начало работать. Удалил -- перестало. Что интересно, было несколько печатей, удаление любой из которых приводило код в нерабочее состояние. Не важно какая именно удалена -- хоть по одной, хоть все вместе. Пока они все есть -- работало, стоило хотя бы одну (любую) удалить -- переставало.

Вся суть вот в этом фрагменте:

class lock_getter
{
   const std::chrono::steady_clock::duration & m_wait_time_limit;
   ...
   std::condition_variable m_wakeup_cv;
   ...
   bool m_access_granted;

public:
   lock_getter(
      ...,
      std::chono::steady_clock::duration wait_time_limit,
      ...)
      : ...
      , m_wait_time_limit{ wait_time_limit }
      , ...
   {}
   ...
private:
   void try_acquire_or_wait()
   {
      ...
      m_access_granted = false;
      m_wakeup_cv.wait_for(m_lock, m_wait_time_limit,
            [this]() { return m_access_granted; });
      if(!m_access_granted)
         throw std::runtime_error{ "lock can't be acquired" };
      ...
   }
};

Нить A пыталась захватить некий ресурс, который ей по запросу должна была отдать нить B. В ожиданнии подтверждения нить A засыпала, как раз в методе try_acquire_or_wait. Нить B точно разрешала нити A захват ресурса и вызывала для m_wakeup_cv метод notify_one (т.е. точно будила нить A). Но проснувшись нить A почему-то считала, что ресурс ей не дали и порождала исключение. Хотя ресурс ей дали. Но нить A все равно считала, что нет, и бросала исключения.

В общем-то, вся разгадка уже на экране. Нужно только внимательно посмотреть :)

Кому лень смотреть, милости прошу под кат.

Дело в том, что wait_time_limit приходит в конструктор по значению, но сохраняется ссылка на это временное значение.

Естественно, что ссылка протухает и при вызове wait_for там оказывается мусор.

При наличии отладочных печатей в этом мусоре были значения, похожие на большие тайм-ауты, и нить A дожидалась уведомления от нити B. А когда отладочные печати удалялись, в этом мусоре был ноль (или околонулевое значение), поэтому wait_for завершался сразу же, а нить B еще не успела ничего сказать нити A.

Почему в объявлении m_wait_time_limit затесался амперсанд, превративший простую константу в константную ссылку, -- я не знаю. Вероятно, просто опечатался. Но компилятор все это проглотил. И даже все тесты при наличии отладочных печатей отработали.

В общем, типичная для C++ ошибка.

PS. Позже в этот день я еще поимел приключений с проявлениями use after free. Но это уже совсем другая история.

Комментариев нет: