C++一直以来缺乏对时间和日期的处理能力,而时间和日期又是现实生活中经常遇到的,C++程序员不得不求助于C,使用笨拙的结构和函数。无法忍受这一情形的程序员则手工构造了自己的实现以满足开发所需,可以想象,有无数的程序员在这方面重复了大量的工作。
而现在,Boost 使用timer、date_time 和chrono 完美地解决了这个问题。
本章介绍timer 和date_time,而chrono 库因为与操作系统联系较密切,将在第10章讲解。

2.1 timer 库概述

timer是一个很小的库,提供简易的度量时间和进度显示功能,可以用于性能测试等需要计时的任务,对于大多数的情况它足够用。
Boost 1.48版以后的 timer库由两个组件组成:早期的timer(V1)和新的cpu_timer(V2),前者使用的是标准 C/C++库函数,而后者则基于 chrono库使用操作系统的 API,计时精度更高。V1 版的 timer 组件计时精度低,但对于 Boost 初学者来说还是具有一定的学习价值,故本章介绍这个 timer组件,而 cpu_timer则放在 10.3节介绍。
timer(V1)库包含三个小组件,分别是:计时器 timer、progress_timer和进度指示器 progress_display,以下将分别详述。

2.2 timer

timer类可以测量时间的流逝,是一个小型的计时器,提供毫秒级别的计时精度和操作函数,供程序员手工控制使用,就像是个方便的秒表。

2.2.1 用法

让我们通过一段示例代码来看一下如何使用timer。

  1. #include <iostream>
  2. #include <boost/timer.hpp>
  3. using namespace std;
  4. using namespace boost;
  5. int main() {
  6. timer t;
  7. // 声明一个计时器对象,开始计时
  8. cout << "max timespan: " << t.elapsed_max() / 3600 << "s" << endl;
  9. // 可度量的最大时间,以秒为单位
  10. cout << "min timespan: " << t.elapsed_min() << "s" << endl;
  11. // 输出已经流逝的时间
  12. cout << "now time elapsed:" << t.elapsed() << "s" << endl;
  13. return 0;
  14. }
  15. /* 程序的输出如下:
  16. max timespan: 2.56205e+09s
  17. min timespan: 1e-06s
  18. now time elapsed:0.000105s
  19. */

上面的代码基本说明了 timer的接口。timer对象一旦被声明,它的构造函数就“启动” 了计时工作,之后就可以随时用 elapsed()函数简单地测量自对象创建后所流逝的时间。成员函数 elapsed_min()返回 timer测量时间的最小精度,elapsed_max()返回 timer能够测量的最大时间范围,两者的单位都是秒。

2.2.2 类摘要

timer 类非常小,全部实现包括所有注释也不过 70 余行,真正的实现代码则只有不到20行。作为我们学习的第一个 Boost组件,值得把源码全部列出来仔细研究:

  1. class timer {
  2. public:
  3. timer() {
  4. _start_time = std::clock();
  5. }
  6. void restart() {
  7. _start_time = std::clock();
  8. }
  9. double elapsed() const {
  10. return double(std::clock() - _start_time) / CLOCKS_PER_SEC;
  11. }
  12. double elapsed_max() const {
  13. return (double((std::numeric_limits<std::clock_t>::max)())
  14. - double(_start_time)) / double(CLOCKS_PER_SEC);
  15. }
  16. double elapsed_min() const {
  17. return double(1) / double(CLOCKS_PER_SEC);
  18. }
  19. private:
  20. std::clock_t _start_time;
  21. };

timer的计时使用了标准库头文件里的 std::clock()函数,它返回自进程启动以来的 clock数,每秒的 clock数则由宏 CLOCKS_PER_SEC定义。
timer的构造函数记录当前的clock数作为计时起点,保存在私有成员变量_start_time中。每当调用elapsed()时就获取此时的clock数,减去计时起点_star_time,再除以CLOCKS_PER_SEC获得以秒为单位的已经流逝的时间。如果调用函数restart(),则重置_start_time重新开始计时。
函数elapsed_min()返回timer能够测量的最小时间单位,是CLOCKS_PER_SEC的倒数。函数 elapsed_max()使用了标准库的数值极限类numeric_limits,获得clock_t类型的最大值,采用类似elapsed()的方式计算可能的最大时间范围。
timer没有定义析构函数,这样做是正确且安全的。因为它仅有一个类型为clock_t的成员变量_start_time,故没有必要实现析构函数来特意“释放资源”(也无资源可供释放)。

2.2.3 使用建议

timer接口简单,轻巧好用,适用于大部分要求不高的程序计时任务。但使用时我们必须理解elapsed_min()和elapsed_max()这两个计时精度函数的含义,它们表明了 timer的能力。
timer不适合高精度的时间测量任务,它的精度依赖于操作系统或编译器,难以做到跨平台。timer也不适合大跨度时间段的测量,如果需要以天、月甚至年作为时间的单位则不能使用timer,应转向10.3节的cpu_timer组件。

2.3 progress_timer

progress_timer也是一个计时器,它派生自timer,会在析构时自动输出时间,省去了timer手动调用elapsed()的工作,是一个用于自动计时相当方便的小工具。

2.3.1 用法

progress_timer继承了timer的全部能力,可以如timer那样使用,例如:

  1. progress_timer t; // 声明一股progress_timer对象
  2. ... // 任意计算、处理工作
  3. cout << t.elapsed() << endl; // 输出流逝的时间

但它有更简单的用法,不需要任何的调用,只要声明progress_timer对象就可以了

  1. #include <boost/progress.hpp>
  2. int main() {
  3. boost::progress_timer t; // 声明对象开始计时
  4. // do something...
  5. } // 退出作用域,调用progress_timer的析构函数

这样,在程序退出(准确地说是离开 main函数局部域)导致 progress_timer析构时,会自动输出流逝的时间。
如果要在一个程序中测量多个时间,可以运用花括号 { } 限定progress_timer的生命期:

  1. {
  2. boost::progress_timer t; // 第一个计时
  3. // do something...
  4. } // progress_timer在这里析构,自动输出时间
  5. {
  6. boost::progress_timer t; // 第二个计时
  7. // do something...
  8. } // progress_timer在这里析构,自动输出时间

只需要声明 progress_timer的实例就可完成所需的全部工作,非常容易。有了progress_timer,程序员今后在做类似性能测试等计算时间的工作时将会感到轻松很多。

2.3.2 类摘要

progress_timer的类摘要如下:

class progress_timer: public timer, noncopyable { 
     explicit progress_timer();  
    progress_timer( std::ostream& os );  
    ~progress_timer(); 
}

progress_timer继承自timer,因此它的接口与timer相同,也很简单。唯一需要注意的是构造函数,它允许将析构时的输出定向到指定的I/O流里,默认是std::cout。如果有特别的需求,可以用其他标准库输出流(ofstream、ostringstream)替换,或者用cout.rdbuf()重定向cout的输出。
例如,下面的代码把progress_timer的输出转移到了stringstream中,它可以被转换为字符串供其他应用使用:

stringstream ss;            //一个字符串对象
{
    progress_timer t(ss);   // 要求 progress_timer输出到 ss中 
}
cout << ss.str();           // progress_timer在这里析构,自动输出时间

2.4 progress_display

progress_display可以在控制台上显示程序的执行进度,如果程序执行很耗费时间,那么它能够提供一个比较友好的用户界面,不至于让用户在等待中失去耐心。

2.4.1 类摘要

progress_display 是一个独立的类,与timer 库中的其他两个组件(timer和progress_timer)没有任何联系,它的类摘要如下:

class progress_display : noncopyable
{
public:
    progress_display( unsigned long expected_count );
    progress_display( unsigned long expected_count,
                      std::ostream& os,
                      const std::string & s1 = "\n",
                      const std::string & s2 = "",
                      const std::string & s3 = "" );
    void restart( unsigned long expected_count );
    unsigned long operator+=( unsigned long increment );
    unsigned long operator++();
    unsigned long count() const;
    unsigned long expected_count() const;
};

progress_display的构造函数接受一个long型的参数expected_count,表示用于进度显示的基数,是最常用的创建progress_display 的方法。
另一种形式的构造函数除了基数和流输出对象外,还接受三个字符串参数,定义显示的三行首字符串。但这个构造函数有点小问题,流输出对象通常都应该是cout,把进度输出到文件或者其他用户看不到的地方似乎没有多大的意义。
在构造后progress_display 对象会显示出一个类似图形化进度条的界面,用两行以字符的形式显示百分比进度,像这样:

0%   10   20   30   40   50   60   70   80   90  100%
|----|----|----|----|----|----|----|----|----|----|

重载的加法操作符operator+=和operator++用来增加计数,并在第三行用字符*显示进度百分比count() / expected_count。成员函数count()可以返回当前的计数,当计数到达基数expected_count时表示任务已经完成,进度为100%。

2.4.2 用法

假设我们要把一些存储在std::vector中的字符串以每个一行的形式写入文本文件中,因为字符串数量很多,而且有的很大,因此可能操作会耗费很多时间。progress_display可以让程序有一个友好的人机界面,让用户知道程序的进度。
使用progress_display首先要调用它的构造函数,传入进度基数,如:progress_display pd( v.size() );随后就可以使用它重载的加法操作符(++pd)来根据程序的运行情况增加计数,直到完成任务。
示范 progress_display 用法的代码如下:

#include <iostream>
#include <fstream>
#include <boost/progress.hpp>

int main() {
    vector<string> v(100);          // 一个字符串向量
    ofstream fs("./test.txt");      // 文件输出流
    // 声明一个progress_display 对象,基数是v 的大小
    progress_display pd(v.size());
    // 开始迭代遍历向量,处理字符串,写入文件
    for (auto &x : v)                // for+auto 循环
    {
        fs << x << endl;
        ++pd;            //更新进度显示
    }
}

当程序处理了75 个字符串时,控制台的显示大概如下:

0%   10   20   30   40   50   60   70   80   90  100%
|----|----|----|----|----|----|----|----|----|----|
**************************************

如果改用progress_display 另一种形式的构造函数,如:progress_display pd(v.size(), cout, “%%%”, “+++”, “???”);
那么显示就会变成这样:

%%%0%   10   20   30   40   50   60   70   80   90   100%
+++|----|----|----|----|----|----|----|----|----|----|
???***************************************************

这种输出形式颇有点“画蛇添足”的味道,建议读者最好不要这么用。

2.4.3 注意事项

progress_display可以用作基本的进度显示,但它有个固有的缺陷:无法把进度显示输出与程序的输出分离。
这是因为progress_display和所有C++程序一样,都向标准输出(cout)输出字符,如果使用progress_display的程序也有输出操作,那么progress_display 的进度显示就会一片混乱。
仍然使用刚才的写入文本文件的例子,我们希望显示进度的同时还能报告空的字符串的行号,像这样:

#include <iostream>
#include <fstream>
#include <boost/progress.hpp>

int main() {
    vector<string> v(100, "aaa");                       // 字符串数组
    v[10] = "";
    v[23] = "";                                         // 两个空字符串
    ofstream fs("./test.txt");
    progress_display pd(v.size());
    for (auto pos = v.begin(); pos != v.end(); ++pos) { // 使用迭代器遍历容器
        fs << *pos << endl;
        ++pd;
        if (pos->empty()) {
            cout << "null string # " << (pos - v.begin()) << endl;
        }
    }
}

似乎一切都很好,但实际的运行结果却是:

0%   10   20   30   40   50   60   70   80   90  100%
|----|----|----|----|----|----|----|----|----|----|
******null string # 10
*******null string # 23
**************************************

很显然,for 循环中的输出打乱了progress_display的正常输出,而progress_display对此毫不知情,仍然“按部就班”地显示着百分比进度。
这个显示混乱的问题很难解决,因为我们无法预知庞大的程序之中哪个地方会存在一个可能会干扰progress_display的输出。一个可能(但远非完美)的办法是在每次显示进度时都调用restart()重新显示进度刻度,然后用operator+=来指定当前进度,而不是简单地调用operator++,例如:

#include <iostream>
#include <fstream>
#include <boost/progress.hpp>

int main() {
    vector<string> v(100, "aaa");                       // 字符串数组
    v[10] = "";
    v[23] = "";                                         // 两个空字符串
    ofstream fs("./test.txt");
    progress_display pd(v.size());
    for (auto pos = v.begin(); pos != v.end(); ++pos) { // 使用迭代器遍历容器
        fs << *pos << endl;
        // 重置进度条后指定当前显示进度
        pd.restart(v.size());
        pd += (pos - v.begin() + 1);

        if (pos->empty()) {
            cout << "null string # " << (pos - v.begin()) << endl;
        }
    }
}

2.5 date_time 库概述

日期和时间在程序中就像整数和字符串一样经常出现,被作为一种基础设施广泛地用在很多地方,例如作为随机数的种子值。但精确并准确地操纵时间非常困难,因为时间本身是一个难以度量的实体,它有许多变化。
我们使用的基本时间度量依据地球的自转,但地球的自转是不均匀的(有时候非常剧烈的地质运动也会影响地球的自转),需要用闰秒、闰月和闰年进行调整①,不同的地区还有夏令时、时区等的人为规定。现实生活中存在着很多个时间度量体系,如儒勒历、格里高利历、农历、印加帝国的太阳历、UTC,等等,非常复杂。想要实现一个可以计算各种时间日期相关问题的库即使不是不可能的,难度也将非常之大。
date_time库勇敢地面对了这个挑战,并成功地解决了大部分问题。它是一个非常全面且灵活的日期时间库,基于我们日常使用的公历(即格里高利历),可以提供时间相关的各种所需功能,如精确定义的时间点、时间段和时间长度、加减若干天/月/年、日期迭代器,等等。date_time 库还支持无限时间和无效时间这种实际生活中有用的概念,而且可以与C 的传统时间结构tm 相互转换,提供向下支持。

2.5.1 使用方式

date_time 库需要编译才能使用,在jamfile里指定lib的语句是:

lib boost_date_time;

date_time库包含两个部分,分别是处理日期的gregorian和处理时间的posix_time,需要包含如下的头文件:

// 处理日期的组件
#include <boost/date_time/gregorian/gregorian.cpp
using namespace boost::gregorian;

// 处理时间的组件
#include <boost/date_time/posix_time/posix_time.hpp>
using namespace boost::posix_time;

2.5.2 基本概念

时间的处理很复杂,所以在使用date_time库之前,我们需要明确一些基本概念。如果把时间想象成一个向前和向后都无限延伸的实数轴,那么时间点就是数轴上的一个点,时间段就是两个时间点之间确定的一个区间,时长(时间长度)则是一个有正负号的标量,它是两个时间点之差,不属于数轴。
时间点、时间段和时长三者之间可以进行运算,例如“时间点+时长=时间点”,“时长+时长=时长”,“时间段∩时间段=时间段”、“时间点∈时间段”,等等,但有的运算也是无意义的,如“时间点+时间点”、“时长+时间段”,等等。这些运算都基于生活常识,很容易理解,但在编写时间处理程序时必须注意。
date_time库支持无限时间和无效时间(NADT,Not Available Date Time)这样特殊的时间概念,类似于数学中极限的含义。时间点和时长都有无限的值,它们的运算规则比较特别,例如“+∞时间点+时长=+∞时间点”,“时间点+∞时长=+∞时间点”。如果正无限值与负无限值一起运算将有可能是无效时间,如“ +∞时长 - ∞时长=NADT ”。
date_time库中用枚举special_values定义了这些特殊的时间概念,它位于名字空间boost::date_time,并被 using语句引入其他子名字空间。

  • pos_infin : 表示正无限;
  • neg_infin : 表示负无限;
  • not_a_date_time : 无效时间;
  • min_date_time : 可表示的最小日期或时间;
  • max_date_time : 可表示的最大日期或时间。

    2.6.2 创建日期对象

    有很多种方式可以创建一个日期对象。

    2.6.2 创建日期对象

    有很多种方式可以创建一个日期对象。
    空的构造函数会创建一个值为not_a_date_time的无效日期;顺序传入年月日值则创建一个对应日期的date 对象,例如:

    date d1;                              // 一个无效的日期
    date d2(2010, 1, 2);                  // 使用数字构造日期
    date d3(2000, Jan, 1);                // 也可以使用英文指定月份
    date d4(d2);                          // date支持拷贝构造
    assert(d1 == date(not_a_date_time));  // 比较一个临时对象
    assert(d2 == d4);                     //date 支持比较操作
    assert(d3 < d4);
    

    date 也允许从一个字符串产生,这需要使用工厂函数from_string()或from_undelimited_string()。前者使用分隔符(斜杠或者连字符)分隔年月日格式的字符串,后者则是无分隔符的纯字符串格式。例如:

    date d1 = from_string("1999-12-31");
    date d2 ( from_string("2015/1/1") );
    date d3 = from_undelimited_string("20011118");
    

    day_clock 是一个天级别的时钟,它也是一个工厂类,调用它的静态成员函数local_day()或universal_day()会返回一个当天的日期对象,分别是本地日期和UTC 日期。day_clock 内部使用了C 标准库的localtime()和gmtime()函数,因此local_day()的行为依赖于操作系统的时区设置。例如:

    cout << day_clock::local_day() << endl;
    cout << day_clock::universal_day() << endl;
    

    我们也可以使用特殊时间概念枚举special_values创建一些特殊的日期,在处理如无限期的某些情形下会很有用:

    date d1(neg_infin);         // 负无限日期
    date d2(pos_infin);         // 正无限日期
    date d3(not_a_date_time);   // 无效日期
    date d4(max_date_time);     // 最大可能日期9999-12-31
    date d5(min_date_time);     // 最小可能日期1400-01-01
    

    使用cout 将它们输出,显示将会是:

    -infinity
    +infinity
    not-a-date-time
    9999-Dec-31
    1400-Jan-01
    

    如果创建日期对象时使用了非法的值,例如日期超出了1400-01-01 到9999-12-31 的范围,或者使用不存在的月份或日期,那么date_time 库会抛出异常(而不是转换为一个无效日期),可以使用what()获得具体的错误信息。
    下面的 date 对象构造均会抛出异常:

    date d1(1399,12,1); //超过日期下限
    date d2(10000,1,1); //超过日期上限
    date d3(2017,2,29); //不存在的日期
    

    2.6.3 访问日期

    date 类的对外接口很像C语言中的 tm 结构,也可以获取它保存的年、月、日、星期等成分,但 date 还提供了更多的操作。
    成员函数year()、month()和day()分别返回日期的年、月、日:

    date d(2017,6,1);
    assert(d.year() == 2017);
    assert(d.month() == 6);
    assert(d.day() == 1);
    

    成员函数year_month_day()返回一个date::ymd_type 结构,可以一次性地获取年月日数据:

    date::ymd_type ymd = d.year_month_day();
    assert(ymd.year == 2017);
    assert(ymd.month == 6);
    assert(ymd.day == 1);
    

    成员函数 day_of_week() 返回date 的星期数,0 表示星期天。day_of_year()返回 date 是当年的第几天(最多是366)。end_of_month()返回当月的最后一天的date 对象:

    cout << d.day_of_week() << endl;            // 输出Thu
    cout << d.day_of_year() << endl;            // 输出152
    assert(d.end_of_month() == date(2017,6,30));
    

    成员函数week_number()返回date 所在的周是当年的第几周,范围是0 至53:

    cout << date(2015,1,10).week_number() << endl; //输出2
    cout << date(2016,1,10).week_number() << endl; //输出1
    cout << date(2017,1,10).week_number() << endl; //输出2
    

    date 还有五个is_xxx()函数,用于检验日期是否是一个特殊日期,它们是:

  • is_infinity() : 是否是一个无限日期;

  • is_neg_infinity(): 是否是一个负无限日期;
  • is_pos_infinity(): 是否是一个正无限日期;
  • is_not_a_date() : 是否是一个无效日期;
  • is_special() : 是否是任意一个特殊日期。

它们的用法如下:

assert(date(pos_infin).is_infinity() );
assert(date(pos_infin).is_pos_infinity() );
assert(date(neg_infin).is_neg_infinity() );
assert(date(not_a_date_time).is_not_a_date() );
assert(date(not_a_date_time).is_special() );
assert(!date(2017,5,31).is_special() );

2.6.4 日期的输出

date对象可以很方便地转换成字符串,它提供了三个自由函数。

  • to_simple_string(): 转换为YYYY-mmm-DD格式的字符串,其中的mm为3字符的英文月份名
  • to_iso_string(): 转换为YYYYMMDD格式的数字字符串
  • to_iso_extended_string(): 转换为YYYY-MM-DD格式的数字字符串

date也支持流输入输出,默认使用YYYY-mmm-DD格式。例如:

date d(2017, 1, 23);
cout << to_simple_string(d) << endl;         // 2017-Jan-23
cout << to_iso_string(d) << endl;            // 20170123
cout << to_iso_extended_string(d) << endl;   // 2017-01-23
cin >> d;    // 2010-1-2     2017-Jan-10  格式需要对
cout << d;   // 2017-Jan-23  2017-Jan-10

2.6.5 转换C结构

date支持与C标准库中的tm结构相互转换,转换的规则和函数如下:

  • to_tm(date): date转换到tm。tm的时分秒成员(tm_hour/tm_min/tm_sec)均置为0,夏令时标志tm_isdst置为-1(表示未知)。
  • date_from_tm(tm datetm): tm转换到date,只使用年、月、日三个成员(tm_year/tm_mon/tm_mday),其他成员均被忽略。