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


– Слушай, ты не помнишь, мы в 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)


  1. quietp
    29.03.2017 15:06
    +2

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


    1. Fil
      29.03.2017 15:17

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


    1. degs
      29.03.2017 21:15
      +2

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


  1. monah_tuk
    30.03.2017 12:37

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


    1. mayorovp
      30.03.2017 12:51

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


      1. monah_tuk
        30.03.2017 13:03

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


      1. Fil
        30.03.2017 13:04

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


        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.


        1. monah_tuk
          30.03.2017 13:07

          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.


          1. Fil
            30.03.2017 13:14

            Раньше были часы 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.


            1. mayorovp
              30.03.2017 13:18

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


              1. monah_tuk
                30.03.2017 13:23

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


              1. Fil
                30.03.2017 13:27

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


                1. mayorovp
                  30.03.2017 13:35

                  Хм...


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

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


            1. monah_tuk
              30.03.2017 13:24

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