Принципы и практика программирования Страуструпа, 3-е издание, графика: почему передача функции обратного вызова в Window::timer_wait дает неожиданные результаты?

В разделе 14.6 (Простая анимация) третьего издания книги Страуструпа «Принципы и практика программирования с использованием C++» используется его графическая библиотека PPP, которая, в свою очередь, использует QT. Я пытаюсь понять, о чем идет речь. Следующая программа работает по назначению: изначально она отображает два черных круга. Через 2 секунды первый круг меняет цвет на красный. Еще через четыре секунды второй круг меняет цвет на синий. Страуструп пишет: «Вы также можете добавить действие («обратный вызов») в качестве аргумента. Это действие вызывается после задержки». Поэтому я поэкспериментировал, заменив указанные строки кода кодом, который это делает. Я ожидал, что результат будет точно таким же, но это не так. Через две секунды ничего не происходит, но через четыре секунды оба круга одновременно меняют цвет.

#include "PPP/Simple_window.h"
#include "PPP/Graph.h"

int main(int /*argc*/, char * /*argv*/[])
{
    using namespace Graph_lib;
    Application app;

    // Window initially displays 2 black circles
    Simple_window w {Point{0,0}, 600, 400, "Callback problem"};
    Circle c1{{175, 200}, 100};
    Circle c2{{425, 200}, 100};
    c1.set_fill_color(Color::black);
    c2.set_fill_color(Color::black);
    w.attach(c1);
    w.attach(c2);

    // Code to be replaced:
    w.timer_wait(2000);    //2000 milliseconds
    c1.set_fill_color(Color::red);
    w.timer_wait(4000);
    c2.set_fill_color(Color::blue);
    //////////////////////////////

    // // I expected this code to have the same result:
    // w.timer_wait(2000, [&]{c1.set_fill_color(Color::red);});
    // w.timer_wait(4000, [&]{c2.set_fill_color(Color::blue);});
    // //////////////////////////////////////////////

    w.wait_for_button();
 }

Почему результат не тот? Могу ли я что-нибудь сделать, чтобы успешно использовать метод обратного вызова?

Вот код для timer_wait:

void WindowPrivate::timer_wait(int milliseconds, std::function<void()> cb)
{
    if (!accept_waits)
        return;
    auto conn = std::make_shared<QMetaObject::Connection>();
    *conn = QObject::connect(&user_timer, &QTimer::timeout,
                     [conn, func = std::move(cb)] {
        QObject::disconnect(*conn);
        func();
    });
    user_timer.start(milliseconds);
}

Пожалуйста, опубликуйте минимальный воспроизводимый пример с Simple_window определением. Второй w.timer_wait() скорее всего заменит первый.

3CxEZiVlQ 27.06.2024 19:41

Аргумент обратного вызова подразумевает, что он будет вызван по истечении заданного времени без блокировки выполнения кода, что и делает только timer_wait() с таймаутом. Наборы инструментов пользовательского интерфейса используют циклы событий, которые требуют, чтобы управление всегда возвращалось к ним как можно скорее для обработки событий, включая обновления пользовательского интерфейса (перерисовку): с помощью timer_wait(2000) вы предотвращаете это, и все будет окончательно выполнено, как только цикл событий сможет быть должным образом обработаны, что wait_for_button(), вероятно, и делает. Кроме того, в вашем коде окончательный результат будет равен 6 секундам, а не 4.

musicamante 27.06.2024 20:01

Мой секундомер показывает 4 секунды.

freeze 27.06.2024 22:13

Ответы @musicamante должны находиться в разделе ответов.

463035818_is_not_an_ai 28.06.2024 00:25

@ 463035818_is_not_an_ai Я не могу дать соответствующий ответ, пока не будет предоставлено надлежащее MRE.

musicamante 28.06.2024 00:51

@musicamante Я надеялся на помощь кого-то, кто знаком с поддержкой PPP3 Страуструпа, возможно, как учителя или ученика. Определение Simple_window включает в себя несколько уровней поверх кода QT, и я признаю, что не могу дать MRE более удовлетворительного результата, чем то, что я уже опубликовал.

freeze 28.06.2024 01:42

@freeze Странно, что там написано 4 секунды, поскольку у вас есть 2000+4000 мс «сна», если только timer_wait внутренне не отслеживает предыдущие интервалы, что кажется странным выбором, но не невозможным (нам нужно посмотреть, как это на самом деле реализовано, вам следует хотя бы предоставить код, относящийся к этой функции). Тем не менее, суть остается: я не могу написать подходящий ответ для этого конкретного случая, но, учитывая поведение, мы, вероятно, сможем отослать вам много похожих постов здесь, на SO (вероятно, начиная с этого, но я оставлю это большему опыту работы с C++ »

musicamante 28.06.2024 02:46

@freeze » люди, чтобы найти более подходящие дубликаты); как уже было сказано, если timer_wait только с аргументом интервала блокируется (а я предполагаю, что это так), это не позволяет циклу событий правильно обрабатывать события и, следовательно, перерисовывать пользовательский интерфейс с новыми изменениями: по умолчанию графические изменения планируют обновление. В стандартном Qt это можно частично обойти, используя QCoreApplication::processEvents(), но это не всегда приемлемо; другие альтернативы основаны на QTimer, что, похоже, и делает timer_wait с обратным вызовом.

musicamante 28.06.2024 02:50

@musicamante Спасибо за ваши комментарии. Я отредактировал свое сообщение, включив в него код реализации функции timer_wait.

freeze 28.06.2024 07:24

код все еще неполный. Хотя user_timer.start(milliseconds); очень похоже на то, что Simple_window имеет один таймер, который ожидает только время, прошедшее при последнем вызове.

463035818_is_not_an_ai 28.06.2024 08:36

@freeze Это функция, которая принимает обратный вызов. Для этой функции должно быть переопределение, которое принимает только тайм-аут, который и вызывает вашу «проблему». Проверьте еще раз, а также проверьте реализацию классов, от которых наследуется Simple_window.

musicamante 28.06.2024 15:12

Мне удалось найти код в Интернете: github.com/villevoutilinen/… (ссылка на (официальном сайте)[ stroustrup.com/PPP3.html]). Блокировка вызвана nested_loop QEventLoop, который теоретически должен позволять обрабатывать события в очереди, но также известно, что вложенные циклы иногда имеют неожиданное поведение. set_fill_color(), похоже, вызывает redraw(), что должно вызвать перерисовку родительского виджета, поскольку он использует QWidget repaint(). Попробуйте вызвать redraw() из c1 сразу после set_fill_color().

musicamante 28.06.2024 15:44

@musicamante Я полностью признаю, что эта дискуссия выходит за рамки моих нынешних способностей. Мой первоначальный вопрос был задан для того, чтобы узнать, неправильно ли я использую графическую библиотеку PPP. Из того, что я здесь понимаю, может показаться, что сама библиотека не выполняет того, что намеревается сделать. В частности, timer_wait при вызове с функцией в качестве аргумента фактически не ожидает заданное время (предположительно из-за неожиданного поведения вложенных циклов).

freeze 28.06.2024 17:00

@musicamante Однако я отмечу, что neged_loop.quit() происходит только в переопределении timer_wait, которое НЕ принимает функцию в качестве аргумента. Моя проблема связана с переопределением, которое это делает.

freeze 28.06.2024 17:01

@freeze Хорошо, теперь я понял: мы просто неправильно поняли себя. Ваша проблема связана с прокомментированным кодом, который показывает одновременное изменение двух кругов. Я отвечу в ближайшее время.

musicamante 28.06.2024 19:40

@musicamante Спасибо за отличный ответ. Мне понадобится некоторое время, чтобы переварить это. Тем временем я нашел обходной путь, который, кажется, работает.

freeze 28.06.2024 20:50
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
16
134
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Проблема вызвана тем, что WindowPrivate всегда использует один и тот же объект QTimer; см. GUI_Private.h:

    QTimer user_timer{&nested_loop};

При последовательном вызове нескольких функций timer_wait(timeout, callback) они фактически вызывают start(msec) таймера:

Запускает или перезапускает таймер с интервалом ожидания в миллисекундах. Если таймер уже запущен, он будет остановлен и перезапущен.

В результате при втором вызове timer_wait(timeout, callback) таймер немедленно сбрасывается и перезапускается с новым тайм-аутом.

Это явно неудовлетворительно для общего использования, но это также понятно из-за простой природы и назначения библиотеки PPP: это базовая библиотека, предназначенная для образовательных целей, поэтому ее возможности несколько ограничены. Не знаю, давала ли реализация в предыдущих версиях библиотеки (до перехода на Qt) другой результат, но, возможно, стоит написать Страуструпсу и сообщить ему об этой проблеме, которую следует как минимум задокументировать, если это не исправлено каким-либо образом.

В реальной программе вы, вероятно, будете использовать два разных таймера, по одному для каждого обратного вызова.

Я считаю, что нет особого смысла пытаться исправить реализацию PPP, но для этого случая вы можете использовать существующие возможности Qt.
В частности, вы можете использовать статические функции QTimer::singleShot(), которые также можно использовать с лямбда-выражениями:

#include <QTimer>

...

    QTimer::singleShot(2000, [&]{c1.set_fill_color(Color::red);});
    QTimer::singleShot(4000, [&]{c2.set_fill_color(Color::blue);});

Имейте в виду важный факт: приведенное выше использование QTimer предполагает, что все, к чему осуществляется доступ внутри функтора, все еще существует, когда он наконец вызывается.

Более подходящее использование статических функций singleShot использует контекст цели (QObject) и автоматически отключает функцию всякий раз, когда она уничтожается.

Альтернативно, реальный объект QTimer может быть создан с родительским элементом (который может быть «отправителем» или «получателем», в зависимости от ситуации), а затем соединить его timeout со связанной функцией.

Другие вопросы по теме