Основные концепции библиотеки chrono (C++)

  • Tutorial

Работа со временем как с безразмерной величиной может приводить к недоразумениям и ошибкам конвертации временных единиц измерения:


– Слушай, ты не помнишь, мы в sleep передаем секунды или миллисекунды?

– Блин, оказывается у меня в часе 360 секунд, ноль пропустил.

Для избежания таких ошибок предусмотрена библиотека chrono (namespace std::chrono). Она была добавлена в C++11 и дорабатывалась в поздних стандартах. Теперь все логично:


using namespace std::chrono;

int find_answer_to_the_ultimate_question_of_life()
{
    //Поиск ответа
    std::this_thread::sleep_for(5s); //5 секунд
    return 42;
}

std::future<int> f = std::async(find_answer_to_the_ultimate_question_of_life);

//Ждем максимум 2.5 секунд
if (f.wait_for(2500ms) == std::future_status::ready)
    std::cout << "Answer is: " << f.get() << "\n";
else
    std::cout << "Can't wait anymore\n";

Библиотека реализует следующие концепции:


  • интервалы времени – duration;
  • моменты времени – time_point;
  • таймеры – clock.

std::ratio


std::ratio – шаблонный класс, реализующий compile-time обыкновенную дробь (m/n). Он не относится к chrono, но активно используется этой библиотекой, поэтому, в первую очередь, познакомимся с ним, чтобы далее не вызывал вопросов.


template<
    std::intmax_t Num,       //Числитель
    std::intmax_t Denom = 1  //Знаменатель
> class ratio;

Важно, что числитель и знаменатель – шаблонные constexpr параметры. Это позволяет формировать тип на этапе компиляции. Этот класс вспомогательный (чисто статический, helper class), и вообще говоря, не предназначен для математических вычислений. Он нужен для эффективного перевода единиц измерений. Например, мы хотим работать с различными единицами расстояний:


template<class _Ratio>
class Length
{
    double length_;
public:
    explicit Length(double length) : length_(length) { }
    double length() const { return length_; }
};

Length<Mm> len1(127.0);
Length<Inches> len2(5.0);
Length<Mm> len3 = len1 + len2;

Пусть миллиметр будет базовой единицей, тогда:


using Mm = std::ratio<1>; //Знаменатель == 1
//Также пользователь может определить те, которые ему нужны:
using Inches = std::ratio<254, 10>;
using Metre = std::ratio<1000, 1>;

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


В связи с вышесказанным, только лишь для полноты примера, я привожу не самую удачную реализацию операции сложения, зато простую:


template<class _Ratio1, class _Ratio2>
Length<Mm> operator+(const Length<_Ratio1> &left, const Length<_Ratio2> &right)
{
    double len =
        left.length() / _Ratio1::den * _Ratio1::num +
        right.length() / _Ratio2::den * _Ratio2::num;
    return Length<Mm>((int)len);
}

Правильно было бы получать метры при сложении метров и километров.


duration — интервал времени


Шаблонный класс std::chrono::duration является типом интервала времени. Интервал времени в chrono — это некоторое количество периодов (в оригинале tick period). Это количество характеризуется типом, например int64_t или float. Продолжительность периода измеряется в секундах и представляется в виде натуральной дроби с помощью std::ratio.


Некоторые популярные интервалы уже определены в библиотеке. Типы могут немного различаться в различных реализациях


using nanoseconds = duration<long long, nano>;
using microseconds  = duration<long long, micro>;
using milliseconds = duration<long long, milli>;
using seconds = duration<long long>;
using minutes = duration<int, ratio<60> >;
using hours = duration<int, ratio<3600> >;

//Приставки nano, micro, milli:
using nano = ratio<1, 1000000000>;
using micro = ratio<1, 1000000>;
using milli = ratio<1, 1000>;

Но можно определить свои:


using namespace std::chrono;

//3-минутные песочные часы
using Hourglass = duration<long, std::ratio<180>>;
//или
using Hourglass =
  duration<long, std::ratio_multiply<std::ratio<3>, minutes::period>>;

//А может вам удобно считать по 2.75 секунд
using MyTimeUnit = duration<long, std::ratio<11, 4>>;

//Нецелое количество секунд. Иногда полезно
using fseconds = duration<float>;

//Для какой-нибудь специфичной платформы
using seconds16 = duration<uint16_t>;

Теперь как с ними работать. Неявная инициализация запрещена:


seconds s = 5; //Ошибка

void foo(minutes);
foo(42); //Ошибка

Только явная:


seconds s{8};

void foo(minutes);
foo(minutes{42});

Кстати, почему используются фигурные скобки можете почитать, например, здесь. Вкратце: для избежания неявного преобразования интегральных типов с потерями. Добавлю еще случай, когда T x(F()); вместо инициализации x, трактуется как объявление функции, принимающей указатель на функцию типа F(*)() и возвращающей T. Решение: T x{F()}; или T x((F()));.


В C++14 добавлены пользовательские литералы для основных единиц:


seconds s = 4min;

void foo(minutes);
foo(42min);

Можно складывать, вычитать и сравнивать:


seconds time1 = 5min + 17s;
minutes time2 = 2h - 15min;
bool less = 59s < 1min;

Как в примере выше, можно неявно преобразовывать часы в минуты, минуты в секунды, секунды в миллисекуну и т. д., но не наоборот:


minutes time3 = 20s; //Ошибка при компиляции
seconds time4 = 2s + 500ms; //Ошибка при компиляции

В общем случае, неявное преобразование для целочисленных типов разрешено если отношение периодов является целым числом:


//(20/15) / (1/3) = 4. Ок!
duration<long, std::ratio<1, 3>> t1 = duration<long, std::ratio<20, 15>>{ 1 };

В противном случае есть 2 способа: округление и преобразование к float-типу.


//Отбрасывание дробной части - округление в сторону нуля
minutes m1 = duration_cast<minutes>(-100s); //-1m

//C++17. Округление в сторону ближайшего целого
minutes m2 = round<minutes>(-100s); //-2m

//C++17. Округление в сторону плюс бесконечности
minutes m3 = ceil<minutes>(-100s); //-1m

//C++17. Округление в сторону минус бесконечности
minutes m4 = floor<minutes>(-100s); //-2m

Второй вариант:


using fminutes = duration<float, minutes::period>;
fminutes m = -100s;

Допустим, для вас избыточно представление количества секунд типом uint64_t. Ок:


using seconds16 = duration<uint16_t, seconds::period>;
seconds16 s = 15s;

Но вы все равно опасаетесь переполнения. Можно использовать класс из библиотеки для безопасной работы с числами. В стандарте такой нет (только предложение), но есть сторонние реализации. Также есть в VS, ее и используем:


#include <safeint.h>

using sint = msl::utilities::SafeInt<uint16_t>;
using safe_seconds16 = duration<sint, seconds::period>;
safe_seconds16 ss = 60000s;
try
{
    ss += 10000s;
}
catch (msl::utilities::SafeIntException e)
{
    //Ой
};

Чтобы вывести значение интервала на экран или в файл, нужно использовать count():


seconds s = 15s;
std::cout << s.count() << "s\n";

Но не используйте count для внутренних преобразований!


time_point — момент времени


Класс time_point предназначен для представления моментов времени. Момент времени может быть охарактеризован как интервал времени, измеренным на каком-либо таймере, начиная с некоторой точки отсчета. Например, если вы готовите суп, пользуясь секундомером, то ваши моменты времени могут быть представлены так:


0 сек: добавить в кастрюлю пассерованные овощи
420 сек: положить картофель
1300 сек: готово

А если по минутной стрелке настенных часов, то те же моменты времени могут быть такими:


17 мин: добавить в кастрюлю пассерованные овощи
24 мин: положить картофель
39 мин: готово

Итак, сам класс:


template<
    class Clock,
    class Duration = typename Clock::duration
> class time_point;

Тип интервала времени нам уже знаком, теперь перейдем к таймеру Clock. В библиотеке 3 таймера:


  1. system_clock – представляет время системы. Обычно этот таймер не подходит для измерения интервалов, так как во время измерения время может быть изменено пользователем или процессом синхронизации. Обычно основывается на количестве времени, прошедших с 01.01.1970, но это не специфицировано.
  2. steady_clock – представляет так называемые устойчивые часы, то есть ход которых не подвержен внешним изменениям. Хорошо подходит для измерения интервалов. Обычно его реализация основывается на времени работы системы после включения.
  3. high_resolution_clock – таймер с минимально возможным периодом отсчетов, доступным системе. Может являтся псевдонимом для одного из рассмотренных (почти наверняка это steady_clock).

У Clock есть статическая переменная is_steady, по который вы можете узнать, является ли таймер монотонным. Также у Clock есть функция now, возвращающая текущий момент времени в виде time_point. Сам по себе объект класса time_point не очень интересен, так как момент его начала отсчета не специфирован и имеет мало смысла. Но к нему можно прибавлять интервалы времени и сравнивать с другими моментами времени:


time_point<steady_clock> start = steady_clock::now();
//или
steady_clock::time_point start = steady_clock::now();
//или
auto start = steady_clock::now();

foo();
if (steady_clock::now() < start + 1s)
    std::cout << "Less than a second!\n";

time_point нельзя сложить с time_point, зато можно вычесть, что полезно для засечения времени:


auto start = steady_clock::now();
foo();
auto end = steady_clock::now();
auto elapsed = duration_cast<milliseconds>(end - start);

Чтобы получить интервал времени, прошедший с момента начала отсчета, можно вызвать time_since_epoch:


auto now = system_clock::now();
system_clock::duration tse = now.time_since_epoch();

Преобразование time_point в число, например для сериализации или вывода на экран, можно осуществить через С-тип time_t:


auto now = system_clock::now();
time_t now_t = system_clock::to_time_t(now);
auto now2 = system_clock::from_time_t(now_t);

Вместо заключения


Самый частый вопрос: как вывести время и дату в читаемом виде. С помощью chrono никак. Можно поиграть с time_t или использовать другую библиотеку от разработчика chrono.

Поделиться публикацией

Похожие публикации

Комментарии 14

    +2
    Филипп: А можете сравнить функционал std::chrono с подмножествами posix_time, local_time из прекрасной библиотекой Jeff Garland-а boost::date_time? Мне кажется что если chrono не поддерживает всех time-related функций из boost::date_time, то рекомендацией по использованию по прежнему должна оставаться библиотека boost-a.
      0

      Я совершенно не знаком с бустовской библиотекой. Про chrono написал потому что ее использует Thread support library. Если нужно не только измерение интервалов, то да, лучше выбрать что-то другое.

        +2
        std::chrono надо сравнивать с boost::chrono и тогда да, они эквивалентны и практически взаимозаменяемы. Обычно этот момент всем трудно дается — то что классы описывающие даты и интервалы времени принадлежат к разным библиотекам.
        0

        Кстати, про steady_clock… Сказано, что он должен быть равномерным. Реализация libstdc++ (gcc) использует CLOCK_MONOTONIC который может замедляться или ускоряться под действием adjtime. Не знаете, насколько это вяжется с равномерностью часов? Насколько я понимаю, да, они не скаканут назад, но измеряемые интервалы могут оказаться неравными (при равенстве duration).

          0

          Где это сказано? Я вижу лишь упоминания о его монотонности.

            0

            Ну я языком я владею плохо. CLOCK_MONOTONIC — тут всё понятно — только возрастает. А steady может переводиться и как равномерный. Или вы про изменение CLOCK_MONOTONIC? То в man clock_gettime. Там есть чисто равномерные часы: CLOCK_MONOTONIC_RAW, на которых не действует adjtime().

              0

              Моя формулировка не совсем точна, сейчас поправлю.


              Objects of class steady_­clock represent clocks for which values of time_­point never decrease as physical time advances and for which values of time_­point advance at a steady rate relative to real time. That is, the clock may not be adjusted.
                0
                advance at a steady rate relative to real time. That is, the clock may not be adjusted.

                собственно вот оно. А это из man clock_gettime:


                       CLOCK_MONOTONIC
                              Clock that cannot be set and represents monotonic time since some unspecified starting point.  This clock is not affected by discontinuous jumps in the system time (e.g., if the system  administrator  manually  changes
                              the clock), but is affected by the incremental adjustments performed by adjtime(3) and NTP.
                  0

                  Раньше были часы std::chrono::monotonic_clock. А VS сначала просто обернули system_clock:


                  class steady_clock
                      : public system_clock
                      {   // wraps monotonic clock
                  public:
                      static const bool is_monotonic = true;  // retained
                      static const bool is_steady = true;
                      };
                  
                  typedef steady_clock monotonic_clock;   // retained

                  Сейчас в VS вроде нормально. Можете проверить последнюю версию gcc и boost::chrono::steady_clock.

                    0

                    Ну, согласно документации, в VS system_clock — монотонные, равномерные и точные. Ничего удивительного, что там все три структуры были синонимами.

                      0

                      Интересно, как там время подводится...

                        0
                        Вы не ошиблись? Я проверил VS 2015 и 2017, заглянул в документацию по 2012 и 2013 и везде:
                        system_clock::is_steady = false;
                        system_clock::is_monotonic = false;
                          0

                          Хм...


                          Часы считаются монотонное Если значение, возвращенное при первом вызове к now(), всегда меньше или равно значение, возвращенное при последующих вызовах для now().
                          Часы считаются постоянной при монотонное и если времени между соседними тактами является постоянной величиной.

                          Видимо, я как-то не так понял этот фрагмент.

                        0

                        в gcc 6.3 так, как я написал выше: clock_gettime() + CLOCK_MONOTONIC. В Clang завтра посмотрю, но исходников 4.0 под рукой нет.

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

              Самое читаемое