Работа со временем как с безразмерной величиной может приводить к недоразумениям и ошибкам конвертации временных единиц измерения:
– Слушай, ты не помнишь, мы в 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 anymoren";
Библиотека реализует следующие концепции:
- интервалы времени –
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() << "sn";
Но не используйте count для внутренних преобразований!
time_point — момент времени
Класс time_point предназначен для представления моментов времени. Момент времени может быть охарактеризован как интервал времени, измеренным на каком-либо таймере, начиная с некоторой точки отсчета. Например, если вы готовите суп, пользуясь секундомером, то ваши моменты времени могут быть представлены так:
0 сек: добавить в кастрюлю пассерованные овощи
420 сек: положить картофель
1300 сек: готово
А если по минутной стрелке настенных часов, то те же моменты времени могут быть такими:
17 мин: добавить в кастрюлю пассерованные овощи
24 мин: положить картофель
39 мин: готово
Итак, сам класс:
template<
class Clock,
class Duration = typename Clock::duration
> class time_point;
Тип интервала времени нам уже знаком, теперь перейдем к таймеру Clock. В библиотеке 3 таймера:
- system_clock – представляет время системы. Обычно этот таймер не подходит для измерения интервалов, так как во время измерения время может быть изменено пользователем или процессом синхронизации. Обычно основывается на количестве времени, прошедших с 01.01.1970, но это не специфицировано.
- steady_clock – представляет так называемые монотонные часы, то есть ход которых не подвержен внешним изменениям. Хорошо подходит для измерения интервалов. Обычно его реализация основывается на времени работы системы после включения.
- 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.
Автор: Филипп Володин