steady_clockでも現在時刻を取得したい! - KAYAC Engineers' Blog

steady_clockでも現在時刻を取得したい!

ソーシャルゲームチームのサーバサイドエンジニアの@kazasikiです。
これはカヤックアドベントカレンダーの3日目の記事です。今年もよろしくお願いします。 それでは早速プログラムの話を始めましょう。前置きが長いのでsteady_clockの挙動とかに詳しい方は結論だけ読んでいただければ幸いです。

まずsteady_clockの紹介

C++にはstd::chrono::steady_clockというクラスがあります。リファレンスから説明を引用します。

steady_clockは、物理的な時間と同様、決して逆行することがない時間を表現するためのクロックである。

通常はC++で時間を扱う場合はstd::chrono::system_clockを使用します。ただし、このクラスは時間が不可逆であることを保証しません。実行環境にもよりますが、例えばOSの時間を変えたりしたときに戻ってしまうかもしれません。そうすると、経過時間によって分岐させるような処理が上手く動かなくなります。また、ベンチマークなどで実行時間を計測したい場合も不都合でしょう。通常はOSの時間を変えたりとかしないと思うのであんまり気にすることではないのですが、基本的には現在時刻とは無関係に経過時間を処理に使いたいという場合はsteady_clockを使う傾向があります。

じゃあsteady_clockを使ってみよう

とりあえず、基本的な実装をしてみましょう。

#include <iostream>
#include <chrono>
#include "unistd.h" // for sleep
using namespace std::chrono;

int main() {
    steady_clock::time_point before = steady_clock::now();
    sleep(4);
    steady_clock::time_point after = steady_clock::now();

    seconds sec = duration_cast<seconds>(after - before);
    std::cout << sec.count() << "秒" << std::endl;
}
% g++ -std=c++11 ./test.cpp && ./a.out
4秒

はい。予想通りの結果になりましたね。

steady_clockでも現在時刻を取得したい!けど、その前に

いやーでもデバッグとかログとかで現在時刻をログに吐きたいことってあると思うんですよ。アプリケーション全体的にsteady_clockを使ってるなら出来たらそれをそのままログに吐いたら現在時刻になってほしいなって思うじゃないんですか。

参考までにまずstd::chrono::system_clock(不可逆を保証しない時計クラス)の出力方法を示します。

#include <iostream>
#include <chrono>
#include <ctime>
#include <iomanip>

using std::chrono::system_clock;

int main() {
    system_clock::time_point p = system_clock::now(); // steady_clockと同様に現在時刻を取得
    time_t t = system_clock::to_time_t(p); // time_t型に変換
    const tm* lt = std::localtime(&t); // それをlocaltimeで日本のタイムロケールに変換
    std::cout << std::put_time(lt, "%c") << std::endl;
    // この工程が既に面倒くさい
}
% g++ -std=c++11 ./test.cpp && ./a.out
Mon Nov 27 10:43:49 2017

このコードを読んで勘の良い方ならお気づきかと思いますが、time_pointをtime_t型に変換するメソッドto_time_tはsystem_clockのメソッドです。そしてその、steady_clock版はもちろん存在しません。ということは、そもそもsteady_clockのtime_pointは標準の時刻型に変換できません。(少なくともsteady_clockクラスにはそういったメソッドはありません)

でも待って!僕達にはtime_point::time_since_epochがついてる!

がっかりするのはまだ早い!time_pointにはtime_since_epochというメソッドが生えてます。試しにsystem_clockに対して使ってみましょう。

#include <iostream>
#include <chrono>

using namespace std::chrono;

int main() {
    system_clock::time_point now = system_clock::now();
    seconds s = duration_cast<seconds>(now.time_since_epoch());
    std::cout << s.count() << std::endl;
}
% g++ -std=c++11 ./test.cpp && ./a.out
1511747519

1511747519はUNIXタイムで2017/11/27 10:51:59です。わーい、大丈夫そう!

では、同じことをsteady_clockにもやってみます。

#include <iostream>
#include <chrono>

using namespace std::chrono;

int main() {
    steady_clock::time_point now = steady_clock::now();
    seconds s = duration_cast<seconds>(now.time_since_epoch());
    std::cout << s.count() << std::endl;
}
% g++ -std=c++11 ./test.cpp && ./a.out
509

あるぇー?めっちゃ小さい値出てきた(・3・)

さて、ここからが問題で、steady_clock::time_point::time_since_epochは何の値を出しているのでしょう? 調べればすぐに分かるのですが、これはマシンの起動時刻から経過時間を出力しています。time_since_epochだけどUnix Epoch Timeとは言ってないって感じです。

uptimeでOSの起動からの経過時間を出力させると一目瞭然です。

% g++ -std=c++11 ./test.cpp && ./a.out && uptime
547
 10:54:15 up 9 min,  1 user,  load average: 0.04, 0.04, 0.02

9min = 540secで544なのでまぁ大体あってますね!

結論

とはいえ、私たちはsteady_clock::time_pointをどうしても現在時間にしたいわけです。そこで便利な方法はこれです。

#include <iostream>
#include <chrono>
#include <ctime>
#include <iomanip>
#include "unistd.h" // for sleep

using namespace std::chrono;

// system_clockを表示する関数
void print_time(const system_clock::time_point &p) {
    time_t t = system_clock::to_time_t(p);
    const tm* lt = std::localtime(&t);
    std::cout << std::put_time(lt, "%c") << std::endl;
}

int main() {
    // 最初にsystem_clockとsteady_clockの両方で現在時間を取得
    system_clock::time_point start1 = system_clock::now();
    steady_clock::time_point start2 = steady_clock::now();

    ::print_time(start1); // 動作確認のため出力

    sleep(4);

    steady_clock::time_point now = steady_clock::now();

    // 開始時のsteady_clock::nowとの差をとって、開始時のsystem_clock::nowに足す
    ::print_time(start1 + duration_cast<seconds>(now - start2));
}
% g++ -std=c++11 ./test.cpp && ./a.out
Mon Nov 27 10:58:28 2017
Mon Nov 27 10:58:32 2017

アプリケーションの開始時に1回だけsystem_clock::nowとsteady_clock::nowを取得しておけば、あとはsteady_clock::nowを使うだけで現在時間が出力できます。毎回system_clock::nowをとって出力すればいいのではとも思いますが、毎回system_clock::nowとsteady_clock::nowのどっちも取得するのはいやだなぁと言うときは使えると思います。(コストの面も気になりますが細かい検証はしてません)

というわけで、steady_clockをメインでつかってるアプリケーションで現在時刻を表示することが出来ました。 めでたし。めでたし。

面白法人カヤックではエンジニアを募集しております!

明日12/4はおかむーの「Qiitaで1000いいねを稼ごうとした人の末路」です!