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

от автора

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

– Слушай, ты не помнишь, мы в 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 натуральную дробь. Он не относится к 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.

ссылка на оригинал статьи https://habrahabr.ru/post/324984/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *