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

в 9:44, , рубрики: c++, chrono, std, время, Программирование, таймер, метки: ,

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

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

  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.

Автор: Филипп Володин

Источник

Поделиться

* - обязательные к заполнению поля