C++进阶:chrono之duration

1,690 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

今天我们来学习一下C++的时间库chrono,chrono是一个C++的一个日期和时间库,源于boost,在C++11时加入,现在已经是C++的一个标准库了。

C++11 中提供的日期和时间相关的库 chrono,通过 chrono 库可以很方便地处理日期和时间,为程序的开发提供了便利。要使用chrono库,需要#include<chrono>,其所有实现均在std::chrono namespace下。 chrono是一个模版库,使用简单,功能强大,chrono 库主要包含三种类型的类:时间间隔(duration)、时钟(clocks)、时间点(time point)。今天我们先学习duration。

std::chrono::duration

duration (持续时间) 是定义为时间刻度数的时间间隔,可以指定一个时间刻度是多少秒。因此,时间刻度是衡量时间长短的基础。duration 模板的实例类型的对象定义了 duration。时间刻度所表示的默认时间间隔是 1 秒,但可以将它定义为更多秒或秒几分之一。例如,如果定义时间刻度为 3600 秒,但意味着 10 个 duration 就是 10 个小时;也能够定义时间刻度为一秒的十分之一,在这种情况下,10 个 duration 表示 1 秒。

也就是说,std::chrono::duration 表示的是一段时间,比如五百年,1024天,两个小时,12.88秒,半个时辰,一炷香的时间等等,只要能换算成秒即可。

// 定义于头文件 <chrono>
template<class Rep, class Period = std::ratio<1>> 
class duration;

std::ratio 类表示每个时钟周期的秒数,其中第一个模板参数 Num 代表分子,Denom 代表分母,该分母值默认为 1,因此,ratio 代表的是一个分子除以分母的数值,比如:ratio<2> 代表一个时钟周期是 2 秒,ratio<60 > 代表一分钟,ratio<60*60 > 代表一个小时,ratio<60*60*24 > 代表一天。而 ratio<1,1000 > 代表的是 1/1000 秒,也就是 1 毫秒,ratio<1,1000000 > 代表一微秒,ratio<1,1000000000 > 代表一纳秒。

Rep参数代表了可以传入的时间单位的类型,可以为float, int, int64等等,如果为float表示可以传入时间单位的一部分,比如传入1.2表示1.2倍个时间单位Period参数代表了时间单位,可以理解为时间的精度,可以为微秒,毫秒,秒,分钟,小时等(或者其它自定义的单位,类型为上面提到的std::ratio)。如果Period参数不指定,默认为 ratio<1>,也就是以秒为单位。

下面用一个简单例子详细说明一下duration的功能:

#include <chrono>
#include <iostream>int main() {
    // 表示秒
    std::chrono::duration<long long, std::ratio<1, 1>> tick_s{15}; // 15秒
    // 表示毫秒
    std::chrono::duration<long long, std::ratio<1, 1000>> tick_ms{1500}; // 1500毫秒
    // 表示微妙
    std::chrono::duration<long long, std::ratio<1, 1000000>> tick_us{150000}; // 150000微妙
    // 表示小时
    std::chrono::duration<double, std::ratio<60, 1>> tick_hour{60};  // 60小时
    //表示1/5秒,也就是0.2秒的单位值
    std::chrono::duration<double, std::ratio<1, 5>> tiny{5.5}; // 5.5x0.2=1.1秒
}

为了方便使用,在标准库中定义了一些常用的时间间隔,比如:时、分、秒、毫秒、微秒、纳秒,它们都位于 chrono 命名空间下,定义如下:

类型定义
std::chrono::nanosecondsduration</*至少 64 位的有符号整数类型*/, std::nano>
std::chrono::microsecondsduration</*至少 55 位的有符号整数类型*/, std::micro>
std::chrono::millisecondsduration</*至少 45 位的有符号整数类型*/, std::milli>
std::chrono::secondsduration</*至少 35 位的有符号整数类型*/>
std::chrono::minutesduration</*至少 29 位的有符号整数类型*/, std::ratio<60>>
std::chrono::hoursduration</*至少 23 位的有符号整数类型*/, std::ratio<3600>>
std::chrono::days (C++20 起)duration</*至少 25 位的有符号整数类型*/, std::ratio<86400>>
std::chrono::weeks (C++20 起)duration</*至少 22 位的有符号整数类型*/, std::ratio<604800>>
std::chrono::months (C++20 起)duration</*至少 20 位的有符号整数类型*/, std::ratio<2629746>>
std::chrono::years (C++20 起)duration</*至少 17 位的有符号整数类型*/, std::ratio<31556952>>

注意:到 hours 为止的每个预定义时长类型至少涵盖 ±292 年的范围。每个预定义时长类型 daysweeksmonthsyears 至少涵盖 ±40000 年范围。 years 等于 365.2425 days (格里高利年的平均长度)。 months 等于 30.436875 daysyears 准确的 1/12 )。

count()成员函数

duration 类还提供了获取时间间隔的时钟周期数的方法 count (),函数原型如下,其主要是返回时间间隔的数值:

constexpr rep count() const;

用法如下:

#include <chrono>
#include <iostream>
int main()
{
    std::chrono::milliseconds ms{3};         // 3 毫秒
    std::chrono::microseconds us = 2*ms;     // 6000 微秒
    // 时间间隔周期为 1/30 秒
    std::chrono::duration<double, std::ratio<1, 30>> hz(3.5);
 
    std::cout <<  "3 ms duration has " << ms.count() << " ticks\n"
              <<  "6000 us duration has " << us.count() << " ticks\n"
              <<  "3.5 hz duration has " << hz.count() << " ticks\n";       
}

输出:

3 ms duration has 3 ticks
6000 us duration has 6000 ticks
3.5 hz duration has 3.5 ticks

类型转换:std::chrono::duration_cast

duration_cast 是 chrono 库提供的一个模板函数,这个函数不属于 duration 类。但是通过这个函数可以对 duration 类对象内部的时钟周期 Period和周期次数的类型 Rep 进行修改,该函数原型如下:

template <class ToDuration, class Rep, class Period>
constexpr ToDuration duration_cast (const duration<Rep,Period>& dtn);

也就是一个 duration 类型是可以被隐式转换为另一个 duration 类型的,比如从秒转换成毫秒,微妙等等,当源类型的时钟周期是目的类型的时钟周期的整数倍时(例如小时到分钟),浮点时长和整数时长间转型能隐式进行无需使用 duration_cast ,,其他情况下都需要通过函数进行转换。下面是一些示例:

例1:分钟转换为毫秒

#include <chrono>
#include <iostream>int main() {
    std::chrono::microseconds us{123456};
    
    // 整数时长:要求 duration_cast
    std::chrono::milliseconds ms =
        std::chrono::duration_cast<std::chrono::milliseconds>(us);
    
    // 小数时长:不要求 duration_cast
    std::chrono::duration<double, std::ratio<1, 1000>> fp_ms = us;
    
    std::cout << "123456 us equals to " << ms.count() << " milliseconds\n";
    std::cout << "123456 us equals to " << fp_ms.count() << " milliseconds\n";
​
    return 0;
}

输出:

123456 us equals to 123 milliseconds
123456 us equals to 123.456 milliseconds

例2

#include <iostream>
#include <chrono>
 
constexpr auto year = 31556952ll; // 格里高利历年的平均秒数
 
int main()
{
    using shakes = std::chrono::duration<int, std::ratio<1, 100000000>>;
    using jiffies = std::chrono::duration<int, std::centi>;
    using microfortnights = std::chrono::duration<float, std::ratio<14*24*60*60, 1000000>>;
    using nanocenturies = std::chrono::duration<float, std::ratio<100*year, 1000000000>>;
 
    std::chrono::seconds sec(1);
 
    std::cout << "1 second is:\n";
 
    // 无精度损失的整数尺度转换:无转型
    std::cout << std::chrono::microseconds(sec).count() << " microseconds\n"
              << shakes(sec).count() << " shakes\n"
              << jiffies(sec).count() << " jiffies\n";
 
    // 有精度损失的整数尺度转换:需要转型
    std::cout << std::chrono::duration_cast<std::chrono::minutes>(sec).count()
              << " minutes\n";
 
    // 浮点尺度转换:无转型
    std::cout << microfortnights(sec).count() << " microfortnights\n"
              << nanocenturies(sec).count() << " nanocenturies\n";
}

输出:

1 second is:
1000000 microseconds
100000000 shakes
100 jiffies
0 minutes
0.82672 microfortnights
0.316887 nanocenturies

例3. 自定义单位转换

#include <chrono>
#include <iostream>

typedef std::chrono::duration<long long, std::ratio<5, 1> > second5s;
typedef std::chrono::duration<long long, std::ratio<1, 10> > one_tenth_seconds;

int main() {
    second5s s =
        std::chrono::duration_cast<second5s>(one_tenth_seconds(300));
    std::cout << "300 [1/10 seconds] equal to " << s.count() << " [5 seconds]\n";
}

算术运算

由于在 duration 类内部做了操作符重载,因此时间间隔之间可以直接进行算术运算,可以将任何二元算术运算符 +、-、*、/、% 应用到 duration 对象上,会得到一个 duration 对象作为结果。这些都是作为非成员运算符函数实现的。下面是一个示例:

#include <chrono>
#include <iostream>

int main() {
    std::chrono::duration<double, std::ratio<1, 5>> tiny{5.5};
    std::chrono::duration<double, std::ratio<1, 5>> small{7.5};
    auto total = tiny + small;
    std::cout << "total = " << total.count() << std::endl;

    auto sub = small - tiny;
    std::cout << "sub = " << sub.count() << std::endl;
}

可以将前缀或后缀自增和自减运算符应用到 duration 对象上,并且可以通过调用成员函数 count() 来得到时间刻度数。下面是示例代码:

#include <chrono>
#include <iostream>

int main() {
    std::chrono::duration<double, std::ratio<1, 5>> tiny{5.5}; // Measured in 1/5 second
    std::chrono::microseconds very_tiny{100}; // Measured in microseconds
    ++tiny;
    very_tiny--;
    std::cout << "tiny = " << tiny.count()
              << " very_tiny = " << very_tiny.count()
              << std::endl;  // tiny = 6.5 very_tiny = 99
}

注意事项:duration 的加减运算有一定的规则,当两个 duration 时钟周期不相同的时候,会先统一成一种时钟,然后再进行算术运算,统一的规则如下:假设有 ratio<x1,y1>ratio<x2,y2 > 两个时钟周期,首先需要求出 x1,x2 的最大公约数 X,然后求出 y1,y2 的最小公倍数 Y,统一之后的时钟周期 ratio 为 ratio<X,Y>。如下示例:

#include <iostream>
#include <chrono>
using namespace std;

int main()
{
    chrono::duration<double, ratio<9, 7>> d1(3);
    chrono::duration<double, ratio<6, 5>> d2(1);
    // d1 和 d2 统一之后的时钟周期
    chrono::duration<double, ratio<3, 35>> d3 = d1 - d2;
}

对于分子 6,、9 最大公约数为 3,对于分母 7、5 最小公倍数为 35,因此推导出的时钟周期为 ratio<3,35>

比较运算

有一整套完整的运算符可以用来比较两个 duration 对象。它们都是由非成员函数实现的,允许比较不同类型的 duration 对象。这个过程可以确定操作数共同的时钟周期,当表示成相同的时钟周期时,就可以比较 duration 的值。例如:

#include <iostream>
#include <ratio>
#include <chrono>

int main ()
{
  std::chrono::duration<int> foo;
  std::chrono::duration<int> bar (10);

                    // counts: foo bar
                    //         --- ---
  foo = bar;                 // 10  10
  foo = foo + bar;           // 20  10
  ++foo;                     // 21  10
  --bar;                     // 21   9
  foo *= 2;                  // 42   9
  foo /= 3;                  // 14   9
  bar += ( foo % bar );      // 14  14

  std::cout << std::boolalpha;
  std::cout << "foo: " << foo.count() << std::endl;
  std::cout << "bar: " << bar.count() << std::endl;
  std::cout << "foo==bar: " << (foo==bar) << std::endl;
  std::cout << "foo > bar: " << (foo > bar) << std::endl;
  std::cout << "foo < bar: " << (foo < bar) << std::endl;

  return 0;
}

输出:

foo: 14
bar: 14
foo==bar: true
foo > bar: false
foo < bar: false

字面量

可以将 duration 常量指定为整数或浮点值,后缀指定了时钟周期。这里有 8 个可以使用的后缀:

  • y是年,例如 3y 或 3.5y,C++20起

    constexpr std::chrono::year operator ""y(unsigned long long y) noexcept
    {
        return std::chrono::year(int(y));
    }
    
  • d 是天,例如 3d 或 3.5d,C++20起

    constexpr std::chrono::day operator ""d(unsigned long long d) noexcept
    {
        return std::chrono::day(d);
    }
    
  • h 是小时,例如 3h 或 3.5h,C++14起,支持整数和浮点数两种类型:

    constexpr std::chrono::hours operator ""h(unsigned long long h)
    {
        return std::chrono::hours(h);
    }
    constexpr std::chrono::duration<long double, ratio<3600,1>> operator ""h(long double h)
    {
        return std::chrono::duration<long double, std::ratio<3600,1>>(h);
    }
    
  • min 是分钟,例如 20min 或 3.5min,C++14起,支持整数和浮点数两种类型

  • s 是秒,例如 10s 或 1.5s,C++14起,支持整数和浮点数两种类型

  • ms 是毫秒,例如 500ms 或 1.5ms,C++14起,支持整数和浮点数两种类型

  • us 是微秒,例如 500us 或 0.5uso,C++14起,支持整数和浮点数两种类型

  • ns 是纳秒,例如 2ns 或 3.5ns,C++14起,支持整数和浮点数两种类型

上面这几个运算符声明于命名空间 std::literals::chrono_literals ,其中 literalschrono_literals 为内联命名空间。能通过 using namespace std::literals 、 using namespace std::chrono_literals 及 using namespace std::literals::chrono_literals 取得对此运算符的访问。

另外,在命名空间 std::chrono 中,标准库提供 using namespace literals::chrono_literals; 指令,故若程序员使用 using namespace std::chrono; 取得对 chrono 库中的类的访问,则对应的字面量运算符亦变为可见。

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono_literals;

    auto day = 24h;
    auto halfhour = 0.5h;
    std::cout << "one day is " << day.count() << " hours\n"
              << "half an hour is " << halfhour.count() << " hours\n";

    auto lesson = 45min;
    auto halfmin = 0.5min;
    std::cout << "one lesson is " << lesson.count() << " minutes\n"
              << "half a minute is " << halfmin.count() << " minutes\n";

    auto snd = 30s;
    std::cout << "half a minute is " << snd.count() << " seconds\n"
              << "a minute and a half is " << (1min + 30s).count() << " seconds\n";

    auto ms1 = 250ms;
    std::chrono::milliseconds ms2 = 1s;
    std::cout << "250ms = " << ms1.count() << " milliseconds\n"
              << "1s = " << ms2.count() << " milliseconds\n";

    auto us1 = 250us;
    std::chrono::microseconds us2 = 1ms;
    std::cout << "250us = " << us1.count() << " microseconds\n"
              << "1ms = " << us2.count() << " microseconds\n";

    auto ns1 = 250ns;
    std::chrono::nanoseconds ns2 = 1us;
    std::cout << "250ns = " << ns1.count() << " nanoseconds\n"
              << "1us = " << ns2.count() << " nanoseconds\n";
}

std::string 亦定义 operator""s ,表示 std::string 类型字面量对象,但它是字符串字面量: 10s 是十秒,而 "10"s 是二字符 string 。

好了,今天的内容到这里了。

参考

duration - C++ Reference (cplusplus.com)