chrono | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要 🔗

このページでは、時間に関する処理を行う機能について取り上げます。たとえば、プログラム内のある処理の実行にかかった時間を測ったり、ある瞬間の時刻を取得したりする機能があります。また、これらの機能を使うときに登場するユーザー定義リテラルについても触れています。

このページの解説は C++14 をベースとしています

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



リングバッファの応用 🔗

前のページの練習問題まで終わった時点で、使用可能なリングバッファが実装できています。そこでこのページでは、リングバッファを使用する側のプログラムを作ってみることにします。

リングバッファを活用できる場面はいくつかありますが、その中から、ストリーミング処理 (streaming processing) の真似事のようなことをしてみたいと思います。ストリーミング処理とは、データを少しずつ受け取りながら、そのデータを使う処理を継続的に進行させるような処理の方法です。たとえば、インターネット上にある動画を再生するとき、データをすべてダウンロードしてから再生するとなると、動画全体がダウンロードされるまで非常に長い時間待つことになります。ストリーミング処理を使った再生(ストリーミング再生)であれば、動画データを少しずつダウンロードしながら、同時に動画の再生を始めていけるため、待ち時間はほとんどなくなります。

つまり、リングバッファの先頭側にあるデータを使って再生を行いながら、同時進行で逆側にデータが追加されていっているわけです。そのため、再生側がデータを消費するよりも速く、データが充填されていかなければ間に合わなくなります。実際にそのようなことが起こると、映像や音声が止まる(固まる)ことになります。

実際にストリーミング再生を実装してみられるといいのですが、ここまでの知識だけでは、インターネットからデータを取得する方法も、動画を再生する方法もありません。そこでここでは真似事に留めることにします。次のような仕様でプログラムを作ります。

読み取りの側にランダムな揺らぎを与えることで、データを使う側の処理の進行に間に合わなくなる可能性を作り出します。

さて、このプログラムを作るにあたって、「一定時間間隔で」という部分を実現する方法は、これまでのページの中に登場していません。そこで、まず時間を取り扱う機能を解説します。そしてページの終わりで上記のような仕様のプログラムを完成させることにします。

<chrono> 🔗

C++ の標準ライブラリには時間に関する機能が用意されています。標準ヘッダは <chrono> です1。<chrono> に含まれているクラスや関数などは、std::chrono名前空間で定義されています。

<chrono> は C++11 で追加された新しい標準ヘッダで、時・分・秒のような単位で表される時刻や時間を取り扱う機能が含まれています。C++14 の時点では、年・月・日のような「日付」に関する機能はありません。

【C++20】大幅に機能追加され、日付やタイムゾーンなどもサポートされるようになっています。

<chrono> のほかに、C言語から引き継いだ時間処理機能も存在しており、<ctime> をインクルードすることで使用できます。基本的に <ctime> の機能を使う必要はありませんが、<chrono> を使っていくと、<ctime> の側にある定義が必要になることがあります。

【C言語プログラマー】C言語の <time.h> にあたるものです。

<chrono> には多数のクラスやクラステンプレートが含まれており、少々複雑になっています。基本的な使い方を理解するにあたって、以下の3つの概念を把握するといいでしょう。

それぞれ以下で解説していきます。

duration 🔗

std::chrono::duration は2つの時間の間隔を表現するための型で、以下のように定義されたクラステンプレートです2

namespace std {
    namespace chrono {
        template <typename Rep, typename Period = std::ratio<1>>
        class duration {
            // ...
        };
    }
}

テンプレート仮引数 Rep は値を表現するために内部で使用する型、Period は値の周期を表す型です。

std::ratio は有理数を表現するためのクラステンプレートで、<ratio> で定義されています。

namespace std {
    template <intmax_t N, intmax_t D = 1>
    class ratio {
        // ...
    };
}

テンプレート仮引数が typename や class でなく、intmax_t という具体的な型名になっています。このようなテンプレートはノンタイプテンプレートと呼ばれ、テンプレート実引数には型ではなく定数を指定できます。ノンタイプテンプレートについては「ノンタイプテンプレート」のページで解説します。

有理数とは、分母と分子の2つの整数を使って表現する実数のことです。ratio<1, 1000> なら 1000分の1 を意味します。

テンプレート仮引数Period が表す周期とは、時間の刻みの細かさ(分解能 (resolution))です。デフォルトでは ratio<1> ですが、2つ目のテンプレート仮引数にデフォルトテンプレート実引数があるので実際には ratio<1, 1> です。これは 1分の1 であり、つまりは 1秒のことです。もし1ミリ秒単位で時間を刻んだ場合の時間間隔を表現したければ、ratio<1, 1000> のように指定します。ただし、ミリ秒単位とか、分単位といったよくある単位には typedef名が用意されているので、そちらを使ったほうが簡単です(あとで取り上げます)。

1000分の1(つまりミリ)や、1分の1000(つまりキロ)のようによく使われる単位については、std::milli や std::kilo といったように、typedef名にしたものが定義されています3

以下は std::chrono::duration を使ったサンプルプログラムです。

#include <chrono>
#include <iostream>

int main()
{
    std::chrono::duration<int> s1 {};
    std::chrono::duration<int> s2 {15};
    std::chrono::duration<int> s3 {s2};
    
    std::cout << s1.count() << "\n"
              << s2.count() << "\n"
              << s3.count() << "\n";

    s1 = s2 + s3;   // 加算
    std::cout << s1.count() << "\n";
}

実行結果:

0
15
15
30

std::chrono::duration のコンストラクタに、Rep の型に変換できる型の値を与えられます。この値は刻んだ時間の回数です。たとえば 15 を渡したときの意味は、この duration の分解能で時間を 15回刻んだということです。分解能が 1秒なら 15秒に相当しますし、分解能が 1ミリ秒なら 15ミリ秒に相当します。「時間を何度刻んだか」という値は countメンバ関数で取得できます。

演算子がオーバーロード(「演算子のオーバーロード」のページを参照)されているため、+ で加算を行うこともできています。このほか -*/% や、それぞれの複合代入演算子、++--、符号演算子の +-、等価演算子と関係演算子 もオーバーロードされています。

【C++20】<< が演算子オーバーロードされており、std::cout << duration; のような方法で簡単に内部の値を出力できるようになりました。

これらの演算子のオペランドは duration です。さきほどのサンプルプログラムのように、duration 同士での計算や比較はできますが、s1 + 10 のようなことはできません(これでは時間の単位が分かりません)。

時間単位の型 🔗

std::chrono::duration はそのまま使ってもいいですが、以下のように、時間単位ごとの typedef名も定義されており、通常はこちらを使ったほうが分かりやすいし簡単です4

namespace std {
    namespace chrono {
        using nanoseconds = duration<処理系定義の符号付き整数型, nano>;
        using microseconds = duration<処理系定義の符号付き整数型, micro>;
        using milliseconds = duration<処理系定義の符号付き整数型, milli>;
        using seconds = duration<処理系定義の符号付き整数型>;
        using minutes = duration<処理系定義の符号付き整数型, ratio<60>>;
        using hours = duration<処理系定義の符号付き整数型, ratio<3600>>;
    }
}

名前のとおりではありますが、それぞれ以下のような意味です。

型名 意味
std::chrono::nanoseconds ナノ秒
std::chrono::microseconds マイクロ秒
std::chrono::milliseconds ミリ秒
std::chrono::seconds
std::chrono::minutes
std::chrono::hours

std::chrono::duration のテンプレート仮引数Rep のところが符号付き整数型になっているので、たとえば std::chrono::seconds を使って 3.5秒のような時間を表現することはできません。小数点以下が必要なのであれば、std::chrono::duration<double> のような型を使うことになります。

また、テンプレート仮引数Period のところには、nano、micro といった名前も並んでいますが、それぞれ std::ratio の typedef名です3

名前 意味
std::nano std::ratio<1, 1000000000>
std::micro std::ratio<1, 1000000>
std::milli std::ratio<1, 1000>

以下のように簡単に時間を表現できます。

#include <chrono>
#include <iostream>

int main()
{
    std::chrono::seconds s1 {15};  // 15秒
    std::chrono::seconds s2 {20};  // 20秒
    std::chrono::seconds s3 {s1 + s2};  // 2つの秒を加算
    std::cout << s3.count() << "\n";

    std::chrono::minutes m1 {3};    // 3分
    std::chrono::seconds s4 {s1 + m1};  // 秒と分を加算。結果は秒
    std::cout << s4.count() << "\n";

    std::chrono::milliseconds ms {s4};  // 秒をミリ秒に変換
    std::cout << ms.count() << "\n";
}

実行結果:

35
195
195000

std::chrono::seconds と std::chrono::minutes を加算することはできますが、その結果は std::chrono::seconds であることに注目してください。秒と分では、より分解能が高い(細かい)のは秒の方ですから、秒と分の演算結果を秒単位では表現できますが、分単位では表現できないためです。

#include <chrono>
#include <iostream>

int main()
{
    std::chrono::seconds s {150};
    std::chrono::minutes m {3};

    std::chrono::minutes result {s + m};  // コンパイルエラー
    std::cout << result.count() << "\n";
}

それでもあえて分解能が低い方の型で結果を受け取りたい場合は std::chrono::duration_cast関数テンプレートで実現できます。名前のとおり、static_cast などのキャストに似せて作られたもので、実引数に渡した duration を、分解能が低い duration に変換して返します。

#include <chrono>
#include <iostream>

int main()
{
    std::chrono::seconds s {150};
    std::chrono::minutes m {3};

    auto result = std::chrono::duration_cast<std::chrono::minutes>(s + m);  // OK. 秒+分の結果を分で受け取る
    std::cout << result.count() << "\n";
}

実行結果:

5

「150秒 + 3分」は 5分30秒ですが、std::chrono::minutes の内部表現は符号付き整数型なので、結果は 5(5分)となります。std::chrono::duration<double, std::ratio<60>> のように、内部の型を指定すれば小数点以下を扱うことは可能です。

#include <chrono>
#include <iostream>

int main()
{
    std::chrono::seconds s {150};
    std::chrono::minutes m {3};

    auto result = std::chrono::duration_cast<std::chrono::duration<double, std::ratio<60>>>(s + m);  // OK. 秒+分の結果を分で受け取る
    std::cout << result.count() << "\n";
}

実行結果:

5.5

時間リテラル 🔗

時間の定数を記述するためのリテラルも用意されています。数値のリテラルに以下のサフィックスを付加することで表現します5

サフィックス 意味 整数の場合の型
ns ナノ秒 std::chrono::nanoseconds
us マイクロ秒 std::chrono::microseconds
ms ミリ秒 std::chrono::milliseconds
s std::chrono::seconds
min std::chrono::minutes
h std::chrono::hours

たとえば、10s とすれば、10秒の意味になり、型は std::chrono::seconds型です。秒以外の場合も同様です。

10.5s のように浮動小数点数を使うことも可能で、10.5秒の意味になります。しかし前述したとおり、std::chrono::seconds などの型の内部表現は符号付き整数型なので、この場合は std::chrono::duration<何らかの型> になります。「何らかの型」は未規定です。

実際にプログラムで使用する際には、using namespace std::literals::chrono_literals; を記述するなどして、std::literals::chrono_literals名前空間にある名前を扱えるようにする必要があります。これが必要になる理由は、こうしたリテラルがどのように実現されているかを知ると理解できますが、その話はあとで取り上げることにします

#include <chrono>
#include <iostream>

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

    auto s1 = 20s;
    auto s2 = -5s;
    std::cout << (s1 + s2).count() << "\n";

    auto m1 = 10min;
    auto m2 = m1 + 2h;  // std::chrono::minutes
    std::cout << m2.count() << "\n";
}

実行結果:

15
130

リテラルから型が分かるので、変数の型を auto で指定できるようになりました。

クロック 🔗

クロック(時計) (clock) は、時間を刻む時計を表現する型です。クロックはクラステンプレートではない通常のクラスですが、複数のクラスが定義されており、目的に合ったものを選んで使用します。

クラス名 意味
std::chrono::system_clock システムの実時間クロック
std::chrono::steady_clock 時間が逆行しないクロック
std::chrono::high_resolution_clock 高分解能のクロック

【C++20】協定世界時を表す std::chrono::utc_clock6、国際原子時を表す std::chrono::tai_clock7、GPS時間を表す std::chrono::gps_clock8、ファイル時間を表す std::chrono::file_clock9 が追加されました。

std::chrono::system_clock10 は、システムのリアルタイムな時間を表現するクロックです。実行環境の時計をそのまま使うようなものであり、時刻合わせされると時間が逆行する可能性がある点が特徴的です。また、<ctime> で使われている std::time_t型と互換性を持っており、std::chrono::system_clock::to_time_t関数や、std::chrono::system_clock::from_time_t関数を用いて相互に変換できます。

std::chrono::steady_clock11 は、時間が逆行しないクロックで、現実世界の時間のようなものです。

std::chrono::high_resolution_clock12 は、システムで使用可能な最も高分解能なクロックです。要するに時間を最も細かく表現できるクロックです。処理系によってはほかの2つのクロックのいずれかの別名に過ぎず、時間が逆行する可能性についても処理系定義です。

Clock::is_steady の値を調べれば、時間が逆行しないことを確認できます。std::chrono::steady_clock::is_steady は必ず true ですが、ほかのクロックは未規定となっています。13

また、それぞれのクロックの分解能がどれだけであるかは処理系定義ですが、クロック Clock の Clock::period がどのように定義されているかを確認すれば判断できます。Clock::period は、std::ratio の typedef名です。

エポック 🔗

クロックの原点となる時刻(クロックの初期状態)のことをエポック (epoch) と呼びます。

(C++14 においては)エポックが具体的に何であるかについては明確な規定はありませんが、ほとんどの処理系では以下のようになっています。

クラス名 エポック
std::chrono::system_clock 1970年1月1日0時0分0秒(UNIX時間)
std::chrono::steady_clock プログラムが起動されたとき
std::chrono::high_resolution_clock いずれかの別名であれば上と同じ。そうでなければ処理系定義

【C++20】std::chrono::system_clock のエポックが 1970年1月1日0時0分0秒であると規定されました14

std::chrono::system_clock は現実のある日時をエポックとしていますが、C++14 においては、閏秒に関する規定がありません。

【C++20】std::chrono::get_leap_second_info関数を使って、ある時点までに挿入されていたはずの閏秒の回数を取得できるようになっています15

time_point 🔗

std::chrono::time_point は時間軸上の1点を表現するための型で、以下のように定義されたクラステンプレートです16

namespace std {
    namespace chrono {
        template <typename Clock, typename Duration = Clock::duration>
        class time_point {
            // ...
        };
    }
}

時間軸上の1点を表現するために、エポックからの経過時間を用います。そのため、エポックを定めるクロックと、経過時間を表現する Duration がテンプレート仮引数になっています。Duration のほうは Clock から自動的に決定させることもできます。

現在の時刻を取得する 🔗

std::chrono::time_point が登場する場面としてよくあるのは、現在の時刻を扱うときです。

現在の時刻は std::chrono::system_clock::now静的メンバ関数17を呼び出すだけで取得できます。この関数の戻り値の型は std::chrono::system_clock::time_point ですが、これは std::chrono::time_point の typedef名なので、単に time_point であると考えればいいです。auto を使って受け取れば気にすることもありません。

auto now = std::chrono::system_clock::now();

前述したとおり、time_point が時間軸上の1点を表す方法は、「エポックからの経過時間」です。そのため、現在の時刻といえば「(yyyy年mm月dd日の)hh時mm分ss秒」のような表現をイメージするかもしれませんがそうではなく、「現在の時刻を、エポックからの経過時間で表現したもの」ということになります。その具体的な値は、std::chrono::time_point::time_since_epoch関数18を使えば、duration として受け取れますが、具体的な時刻を想像することは難しいです。

#include <chrono>
#include <iostream>

int main()
{
    auto now = std::chrono::system_clock::now();
    auto elapsed = now.time_since_epoch();
    std::cout << elapsed.count() << "\n";
}

実行結果:

17299365231640632

分かりやすいかたちで現在の時刻を知るには、std::chrono::system_clock::now静的メンバ関数が返した結果を、このページの冒頭で少し触れた <ctime> にある関数を使って整形して出力します。

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

int main()
{
    auto now = std::chrono::system_clock::now();
    std::time_t time {std::chrono::system_clock::to_time_t(now)};
    std::cout << std::ctime(&time) << "\n";
}

実行結果:

Sat Oct 26 19:15:32 2024

まず、std::chrono::system_clock::to_time_t関数19に time_point を渡すと、std::time_t型で表現した値に変換して返ってきます。std::time_t型は <ctime> で定義されている型で、具体的にどのような型かは処理系定義ですが、ともかくエポックからの経過時間を表現した型です。

こうして std::time_t型に直せば、<ctime> にある std::ctime関数20を使って、日時の文字列表現を得ることができるようになります。この関数が返す文字列は「Www Mmm dd hh:mm:ss yyyy」(曜日 月 日 時:分:秒 年)の形式です。最後に改行文字が付きます。

時間を進める 🔗

std::chrono::time_point には += と -= や +、- の演算子オーバーロードがあり、時間を進めたり戻したりできます。相手側の引数は duration です。

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

void print_time(std::chrono::system_clock::time_point time)
{
    auto t = std::chrono::system_clock::to_time_t(time);
    std::cout << std::ctime(&t);
}

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

    auto time = std::chrono::system_clock::now();
    print_time(time);

    time += 1h;
    print_time(time);

    time -= 30min;
    print_time(time);
}

実行結果:

Sat Oct 26 19:47:33 2024
Sat Oct 26 20:47:33 2024
Sat Oct 26 20:17:33 2024

時間を比較する 🔗

time_point 同士を ==!=<<=>>= で比較できます。比較する相手と Clock が同一でなければなりません。

#include <chrono>
#include <iostream>

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

    auto time1 = std::chrono::steady_clock::now();
    auto time2 = time1;
    std::cout << std::boolalpha
              << (time1 == time2) << "\n"
              << (time1 != time2) << "\n"
              << (time1 < time2) << "\n"
              << (time1 <= time2) << "\n"
              << (time1 > time2) << "\n"
              << (time1 >= time2) << "\n";
    std::cout << "\n";

    time2 += 30min;
    std::cout << std::boolalpha
              << (time1 == time2) << "\n"
              << (time1 != time2) << "\n"
              << (time1 < time2) << "\n"
              << (time1 <= time2) << "\n"
              << (time1 > time2) << "\n"
              << (time1 >= time2) << "\n";
    std::cout << "\n";

    time2 -= 1h;
    std::cout << std::boolalpha
              << (time1 == time2) << "\n"
              << (time1 != time2) << "\n"
              << (time1 < time2) << "\n"
              << (time1 <= time2) << "\n"
              << (time1 > time2) << "\n"
              << (time1 >= time2) << "\n";
    std::cout << "\n";
}

実行結果:

true
false
false
true
false
true

false
true
true
true
false
false

false
true
false
false
true
true

経過した時間を調べる 🔗

プログラム内のある処理の実行にかかった時間を計測するためにも、クロックの now静的メンバ関数が利用できます。時間を計測したい処理の前後で now静的メンバ関数を呼んで、返ってきた値の差を取れば、経過した時間の duration が得られます。

ここで使うクロックは system_clock でもいいですが、時間が逆行する可能性がある点を踏まえると、steady_clock を使う方が自然かもしれません。あるいは高精度に計測するために、high_resolution_clock を使う選択肢もあります。

high_resolution_clock は時間が逆行するかどうかも、そもそも本当にほかのクロックよりも高精度なのかどうかも処理系定義であることに注意してください(「クロック」の項を参照)。

具体的なプログラムは次のようになります。

#include <chrono>
#include <iostream>

int main()
{
    auto begin = std::chrono::steady_clock::now();

    // 適当な処理
    long long n {0};
    for (unsigned long long i {0}; i < 100000000UL; ++i) {
        n *= i;
    }

    auto end = std::chrono::steady_clock::now();

    auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
    std::cout << elapsed.count() << "\n";
}

実行結果:

133

ユーザー定義リテラル 🔗

このページで時間のリテラルが登場しましたが、これを実現している機能がユーザー定義リテラル (user defined literal) です。

ユーザー定義リテラルは、リテラルに自由な意味を持たせる機能です。時間のリテラルの場合、30min が 30分を意味するようになります。これは本来ただの整数リテラルである 30 に対して、min というサフィックスを付加することによって意味付けしているということです。

ユーザー定義リテラルは我々も自由に使うことができる機能です。つまり新しい意味を持ったリテラルを追加できます。構文は次のとおりで、演算子のオーバーロードとよく似ています。

// 宣言
operator"" サフィックス名(仮引数の並び);

// 定義
operator"" サフィックス名(仮引数の並び)
{
}

こうして定義される operator"" サフィックス名 という名前の関数を、リテラル演算子 (literal operator) と呼びます。なおこれを記述できるのは、名前空間スコープ(「スコープと名前空間」のページを参照)に限られます。

「サフィックス名」は _ で始まるようにします。頭文字が _ で始まらないサフィックス名は C++ の標準機能で使うために予約されています21

「仮引数の並び」の部分の記述の違いによって対象のリテラルの種類が決まります。選択肢は以下の4つで、このあとそれぞれ解説します。

【上級】文字リテラルと文字列リテラルには、char、wchar_t、char16_t、char32_t の各型のものが含まれます。

ユーザー定義整数リテラル

整数リテラルに対するユーザー定義リテラルの宣言方法は3通りあります。いずれも、仮に「サフィックス名」を _X とすると、123_X のように整数リテラルを記述すると、宣言されているリテラル演算子のいずれかが呼び出されます。基数をあらわすプリフィックス(「ビット単位の処理」のページを参照)が付いていても構いません。

複数のリテラル演算子を同時に宣言している場合は、以下で説明する1つ目の方法が優先され、次いで2つ目の方法、最後に3つ目の方法が選ばれます。1つ目の方法で宣言していないときに、2つ目と3つ目の方法が両方とも宣言されている場合はコンパイルエラーとなります22

1つ目は以下の方法です。仮引数の名前は便宜的に付けたものです。123_X であれば、123 が渡されてきます。

operator"" サフィックス名(unsigned long long n);

仮引数n の型は必ず unsigned long long でなければならず、ほかの整数型を選ぶことはできません。符号無しであることは注目すべき点で、渡されてくる値は必ず正の整数です。これはそもそも、符号をあらわす - は演算子なのであって、整数リテラルの一部ではないからです(「計算」のページを参照)。-123_X に対しては 123 が渡されてくることになり、結果の型の値に対して - の演算子が適用されます。

時間のリテラルでもこの方法が使われています。「分」を表す min を例に取ると、次のように定義されています。

namespace std {
    inline namespace literals {
        inline namespace chrono_literals {
            std::chrono::minutes operator"" min(unsigned long long m)
            {
                return std::chrono::minutes(m);
            }
        }
    }    
}

たくさんの名前空間で囲まれていますが、ユーザー定義リテラルの影響範囲を制御するためです。明示的に using namespace std::literals::chrono_literals; のような指定を行って、名前空間内にある名前を使用可能な状態にしなければ、operator"" min というリテラル演算子が勝手に機能することはありません。独自にユーザー定義リテラルを作るときもこれに似せて、名前空間で囲むといいです。


2つ目は以下の方法です。仮引数の名前は便宜的に付けたものです。この記法では、123_X に対して、"123" という終端文字付きの文字列が渡されてきます。

operator"" サフィックス名(const char* n);

整数リテラルなのに文字列で渡されてくるのは不思議ですが、unsigned long long型でも表現できないほど大きな数に対応できるようにするためです。

文字列から整数型に変換する必要があれば std::stoi関数などを使えます(「文字列操作」のページを参照)。

このかたちのリテラル演算子のことを、生のリテラル演算子 (raw literal operator) と呼びます。生のリテラル演算子は、ユーザー定義浮動小数点数リテラルでも使用されることに注意が必要です。つまり、123_X でも 123.4_X でも生のリテラル演算子を使おうとする可能性があります。


3つ目は、これまでのページで取り上げていないテンプレートパラメータパックという機能を使ったものですが、紹介だけしておきます。この方法では、123_X に対して、'1''2''3' のように各文字が別個になって C に渡されてきます。

template <char... C>
operator"" サフィックス名();

このかたちのリテラル演算子のことを、リテラル演算子テンプレート (literal operator template) と呼びます。この方法も、ユーザー定義浮動小数点数リテラルとしても機能することに注意が必要です。

ユーザー定義浮動小数点数リテラル

浮動小数点数リテラルに対するユーザー定義リテラルの宣言方法は3通りあります。いずれも、仮に「サフィックス名」を _X とすると、1.23_X のように浮動小数点数リテラルを記述すると、宣言されているリテラル演算子のいずれかが呼び出されます。

複数のリテラル演算子を同時に宣言している場合は、以下で説明する1つ目の方法が優先され、次いで2つ目の方法、最後に3つ目の方法が選ばれます。1つ目の方法で宣言していないときに、2つ目と3つ目の方法が両方とも宣言されている場合はコンパイルエラーとなります23

1つ目は以下の方法です。仮引数の名前は便宜的に付けたものです。1.23_X であれば、1.23 が渡されてきます。

operator"" サフィックス名(long double f);

仮引数f の型は必ず long double でなければならず、ほかの浮動小数点型を選ぶことはできません。

この方法は時間のリテラルでも使われており、たとえば 0.5min で 0.5分を表せます。

namespace std {
    inline namespace literals {
        inline namespace chrono_literals {
            std::chrono::duration<処理系定義の型, ratio<60,1>> operator"" min(long double m)
            {
                return std::chrono::duration<処理系定義の型, ratio<60,1>>(m);
            }
        }
    }    
}


2つ目の方法は、ユーザー定義整数リテラルのところにも登場した、生のリテラル演算子です。この方法では、1.23_X に対して、"1.23" という終端文字付きの文字列が渡されてきます。

operator"" サフィックス名(const char* n);

文字列から浮動小数点型に変換する必要があれば std::stod関数などを使えます(「文字列操作」のページを参照)。

生のリテラル演算子は、ユーザー定義整数リテラルでも使用されることに注意が必要です。つまり、123_X でも 123.4_X でも生のリテラル演算子を使おうとする可能性があります。


3つ目の方法は、ユーザー定義整数リテラルのところにも登場した、リテラル演算子テンプレートです。この方法では、1.23_X に対して、'1''.''2''3' のように各文字が別個になって C に渡されてきます。

template <char... C>
operator"" サフィックス名();

リテラル演算子テンプレートは、ユーザー定義整数リテラルでも使用されることに注意が必要です。

ユーザー定義文字列リテラル

文字列リテラルに対するユーザー定義リテラルは以下のように宣言します。仮引数の名前は便宜的に付けたものです。

operator"" サフィックス名(const char* str, std::size_t len);

仮に「サフィックス名」を _X として、"xyz"_X のように文字列リテラルを記述すると、宣言されているリテラル演算子が呼び出されます。仮引数str には文字列リテラルの内容(この例では、終端文字付きの "xyz")、仮引数len には文字列リテラルの長さ(この例では 3)が渡されてきます。

【上級】ワイド文字列リテラルに対しては仮引数str の型を const wchar_t* に、char16_t文字列リテラルに対してなら const char16_t*、char32_t文字列リテラルに対しては const char32_t* にします。

この方法は <string> でも使われています24

namespace std {
    inline namespace literals {
        inline namespace string_literals {
            string operator"" s(const char* str, std::size_t len)
            {
                return string{str, len};
            }
        }
    }    
}

つまり、"xyz"s のように文字列リテラルを記述すると std::string を得られます。

#include <iostream>
#include <string>

int main()
{
    using namespace std::literals::string_literals;

    auto s = "xyz"s;  // std::string
    std::cout << s.length() << "\n";
}

実行結果:

3

ユーザー定義文字リテラル

文字リテラルに対するユーザー定義リテラルは以下のように宣言します。仮引数の名前は便宜的に付けたものです。

operator"" サフィックス名(char ch);

仮に「サフィックス名」を _X として、'a'_X のように文字リテラルを記述すると、宣言されているリテラル演算子が呼び出されます。仮引数ch には文字リテラルの内容(この例では、'a')が渡されてきます。

【上級】ワイド文字リテラルに対しては仮引数str の型を wchar_t に、char16_t文字リテラルに対してなら char16_t、char32_t文字リテラルに対しては char32_t にします。

リングバッファの実験 🔗

それでは最後に、このページの最初で考えていたストリーミング処理の真似事をするプログラムを作ってみます。ランダムな秒数だけ待ちながら、ファイルからの読み取りと、標準出力への書き出しを行います。この2つの処理のあいだをリングバッファが取り持つ構図です。リングバッファの実装は、前のページの練習問題で作った最新のものをそのまま使っています。

// ring_buffer.h
#ifndef RING_BUFFER_H_INCLUDED
#define RING_BUFFER_H_INCLUDED

#include <algorithm>
#include <cassert>
#include <vector>

namespace mylib {

    // リングバッファ
    template <typename T>
    class RingBuffer {

        template <typename T>
        friend class RingBuffer;

    public:
        // イテレータ
        class Iterator {
        public:
            using value_type = T;                       // 要素型
            using reference = value_type&;              // 要素の参照型
            using const_reference = const value_type&;  // 要素の参照型
            using pointer = value_type*;                // 要素のポインタ型
            using const_pointer = const value_type*;    // 要素の constポインタ型
            using size_type = std::size_t;              // サイズ型
            using difference_type = std::ptrdiff_t;     // 距離型

        public:
            // コンストラクタ
            Iterator(RingBuffer& body, size_type pos, bool is_past_the_end);


            // ==演算子
            inline bool operator==(const Iterator& rhs) const
            {
                return m_body == rhs.m_body
                    && m_pos == rhs.m_pos
                    && m_is_past_the_end == rhs.m_is_past_the_end;
            }

            // !=演算子
            inline bool operator!=(const Iterator& rhs) const
            {
                return !(*this == rhs);
            }

            // *演算子(間接参照)
            inline reference operator*()
            {
                return *common_get_elem_ptr(this);
            }

            // *演算子(間接参照)
            inline const_reference operator*() const
            {
                return *common_get_elem_ptr(this);
            }

            // ->演算子
            inline pointer operator->()
            {
                return common_get_elem_ptr(this);
            }

            // ->演算子
            inline const_pointer operator->() const
            {
                return common_get_elem_ptr(this);
            }

            // ++演算子(前置)
            Iterator& operator++();

            // ++演算子(後置)
            Iterator operator++(int);


        private:
            template <typename T>
            inline static auto* common_get_elem_ptr(T* self)
            {
                return &self->m_body.m_data[self->m_pos];
            }

        private:
            RingBuffer&             m_body;             // 本体のリングバッファへの参照
            size_type               m_pos;              // 指し示している要素の位置
            bool                    m_is_past_the_end;  // 終端の次を指すイテレータか
        };

        // constイテレータ
        class ConstIterator {
        public:
            using value_type = T;                       // 要素型
            using const_reference = const value_type&;  // 要素の参照型
            using const_pointer = const value_type*;    // 要素の constポインタ型
            using size_type = std::size_t;              // サイズ型
            using difference_type = std::ptrdiff_t;     // 距離型

        public:
            // コンストラクタ
            ConstIterator(const RingBuffer& body, size_type pos, bool is_past_the_end);


            // ==演算子
            inline bool operator==(const ConstIterator& rhs) const
            {
                return m_body == rhs.m_body
                    && m_pos == rhs.m_pos
                    && m_is_past_the_end == rhs.m_is_past_the_end;
            }

            // !=演算子
            inline bool operator!=(const ConstIterator& rhs) const
            {
                return !(*this == rhs);
            }

            // *演算子(間接参照)
            inline const_reference operator*() const
            {
                return *common_get_elem_ptr(this);
            }

            // ->演算子
            inline const_pointer operator->() const
            {
                return common_get_elem_ptr(this);
            }

            // ++演算子(前置)
            ConstIterator& operator++();

            // ++演算子(後置)
            ConstIterator operator++(int);


        private:
            template <typename T>
            inline static auto* common_get_elem_ptr(T* self)
            {
                return &self->m_body.m_data[self->m_pos];
            }

        private:
            const RingBuffer&       m_body;             // 本体のリングバッファへの参照
            size_type               m_pos;              // 指し示している要素の位置
            bool                    m_is_past_the_end;  // 終端の次を指すイテレータか
        };

    public:
        using container_type = typename std::vector<T>;                     // 内部コンテナの型
        using value_type = typename container_type::value_type;             // 要素型
        using reference = typename container_type::reference;               // 要素の参照型
        using const_reference = typename container_type::const_reference;   // 要素の const参照型
        using pointer = typename container_type::pointer;                   // 要素のポインタ型
        using const_pointer = typename container_type::const_pointer;       // 要素の constポインタ型
        using size_type = typename container_type::size_type;               // サイズ型
        using iterator = Iterator;                                          // イテレータ型
        using const_iterator = ConstIterator;                               // constイテレータ型

    public:
        // コンストラクタ
        //
        // size: 容量
        explicit RingBuffer(size_type capacity);

        // テンプレート変換コンストラクタ
        template <typename U>
        RingBuffer(const RingBuffer<U>& other);


        // コピー代入演算子
        RingBuffer& operator=(const RingBuffer& rhs) = default;
        
        // ==演算子
        bool operator==(const RingBuffer& rhs) const;

        // !=演算子
        inline bool operator!=(const RingBuffer& rhs) const
        {
            return !(*this == rhs);
        }


        // 要素を追加
        void push_back(const value_type& value);

        // 要素を取り除く
        void pop_front();

        // 空にする
        void clear();


        // 先頭の要素の参照を返す
        inline reference front()
        {
            return common_front(this);
        }

        // 先頭の要素の参照を返す
        inline const_reference front() const
        {
            return common_front(this);
        }

        // 末尾の要素の参照を返す
        inline reference back()
        {
            return common_back(this);
        }

        // 末尾の要素の参照を返す
        inline const_reference back() const
        {
            return common_back(this);
        }



        // 先頭の要素を指す イテレータを返す
        inline iterator begin()
        {
            return iterator(*this, m_front, empty());
        }

        // 末尾の要素の次を指す イテレータを返す
        inline iterator end()
        {
            return iterator(*this, m_back, true);
        }

        // 先頭の要素を指す constイテレータを返す
        inline const_iterator begin() const
        {
            return const_iterator(*this, m_front, empty());
        }

        // 末尾の要素の次を指す constイテレータを返す
        inline const_iterator end() const
        {
            return const_iterator(*this, m_back, true);
        }


        // 容量を返す
        inline size_type capacity() const
        {
            return m_data.size();
        }

        // 要素数を返す
        inline size_type size() const
        {
            return m_size;
        }

        // 空かどうかを返す
        inline bool empty() const
        {
            return m_size == 0;
        }

        // 満杯かどうかを返す
        inline bool full() const
        {
            return m_size == capacity();
        }

    private:
        // 次の位置を返す
        inline size_type get_next_pos(size_type pos) const
        {
            return (pos + 1) % capacity();
        }

        // 手前の位置を返す
        inline size_type get_prev_pos(size_type pos) const
        {
            if (pos >= 1) {
                return pos - 1;
            }
            else {
                return m_size - 1;
            }
        }

        template <typename T>
        inline static auto& common_front(T* self)
        {
            assert(self->empty() == false);
            return self->m_data[self->m_front];
        }

        template <typename T>
        inline static auto& common_back(T* self)
        {
            assert(self->empty() == false);
            return self->m_data[self->get_prev_pos(self->m_back)];
        }

    private:
        container_type              m_data;         // 要素
        size_type                   m_size{0};      // 有効な要素の個数
        size_type                   m_back{0};      // 次に push される位置
        size_type                   m_front{0};     // 次に pop される位置
    };


    // コンストラクタ
    template <typename T>
    RingBuffer<T>::RingBuffer(size_type capacity) : m_data(capacity)
    {
    }

    // コンストラクタ(異なる要素型の RingBuffer から作成)
    template <typename T>
    template <typename U>
    RingBuffer<T>::RingBuffer(const RingBuffer<U>& other) :
        m_data(other.m_data.capacity()),
        m_size {other.m_size},
        m_back {other.m_back},
        m_front {other.m_front}
    {
        std::transform(
            std::cbegin(other.m_data),
            std::cend(other.m_data),
            std::begin(m_data),
            [](const auto& e) {
                return static_cast<T>(e);
            }
        );
    }

    // ==演算子
    template <typename T>
    bool RingBuffer<T>::operator==(const RingBuffer& rhs) const
    {
        return m_data == rhs.m_data
            && m_size == rhs.m_size
            && m_back == rhs.m_back
            && m_front == rhs.m_front;
    }

    // 要素を追加
    template <typename T>
    void RingBuffer<T>::push_back(const value_type& value)
    {
        if (full()) {
            m_front = get_next_pos(m_front);
        }
        else {
            m_size++;
        }

        m_data[m_back] = value;
        m_back = get_next_pos(m_back);
    }

    // 要素を取り除く
    template <typename T>
    void RingBuffer<T>::pop_front()
    {
        assert(empty() == false);

        m_front = get_next_pos(m_front);
        --m_size;
    }

    // 空にする
    template <typename T>
    void RingBuffer<T>::clear()
    {
        m_size = 0;
        m_back = 0;
        m_front = 0;
    }




    // ------------ Iterator ------------
    // コンストラクタ
    template <typename T>
    RingBuffer<T>::Iterator::Iterator(RingBuffer& body, size_type pos, bool is_past_the_end) :
        m_body {body},
        m_pos {pos},
        m_is_past_the_end {is_past_the_end}
    {

    }

    // ++演算子(前置)
    template <typename T>
    typename RingBuffer<T>::Iterator& RingBuffer<T>::Iterator::operator++()
    {
        assert(!m_is_past_the_end);

        m_pos = m_body.get_next_pos(m_pos);

        // 終端要素の位置を越えた?
        if (m_body.get_next_pos(m_pos) == m_body.get_next_pos(m_body.end().m_pos)) {
            m_is_past_the_end = true;
        }

        return *this;
    }

    // ++演算子(後置)
    template <typename T>
    typename RingBuffer<T>::Iterator RingBuffer<T>::Iterator::operator++(int)
    {
        Iterator tmp {*this};
        ++(*this);
        return tmp;
    }


    // ------------ ConstIterator ------------
    // コンストラクタ
    template <typename T>
    RingBuffer<T>::ConstIterator::ConstIterator(const RingBuffer& body, size_type pos, bool is_past_the_end) :
        m_body {body},
        m_pos {pos},
        m_is_past_the_end {is_past_the_end}
    {

    }

    // ++演算子(前置)
    template <typename T>
    typename RingBuffer<T>::ConstIterator& RingBuffer<T>::ConstIterator::operator++()
    {
        assert(!m_is_past_the_end);

        m_pos = m_body.get_next_pos(m_pos);

        // 終端要素の位置を越えた?
        if (m_body.get_next_pos(m_pos) == m_body.get_next_pos(m_body.end().m_pos)) {
            m_is_past_the_end = true;
        }

        return *this;
    }

    // ++演算子(後置)
    template <typename T>
    typename RingBuffer<T>::ConstIterator RingBuffer<T>::ConstIterator::operator++(int)
    {
        Iterator tmp {*this};
        ++(*this);
        return tmp;
    }
}

#endif
//main.cpp
#include <chrono>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <random>
#include <string>
#include "ring_buffer.h"

#define READ_LOG_ENABLED    1   // 読み取り時のログを出力する

namespace {


#if READ_LOG_ENABLED == 1
    void print_readlog(const char* s)
    {
        std::cout << s;
    }
#else
    void print_readlog(const char* s)
    {
    }
#endif


    constexpr auto read_interval = std::chrono::seconds{1};     // ファイルからの読み込みを行う時間間隔 
    constexpr auto write_interval = std::chrono::seconds{3};    // 標準出力への書き出しを行う時間間隔 
    constexpr unsigned int read_line_num_min {1};   // 1回に読み込む最小行数
    constexpr unsigned int read_line_num_max {3};   // 1回に読み込む最大行数
    constexpr unsigned int write_line_num {6};      // 1回に書き出す行数
    constexpr std::size_t ring_buffer_size {64};    // リングバッファの要素数

}

int main()
{
    std::random_device rand_dev {};
    std::mt19937 rand_engine(rand_dev());

    constexpr auto path = "input_data.txt";
    std::ifstream ifs(path);
    if (!ifs) {
        std::cerr << "File open error: " << path << "\n";
        return EXIT_FAILURE;
    }

    mylib::RingBuffer<std::string> ring_buf {ring_buffer_size};

    auto next_read_time = std::chrono::steady_clock::now() + read_interval;
    auto next_write_time = std::chrono::steady_clock::now() + write_interval;

    while (true) {
        
        const auto now = std::chrono::steady_clock::now();

        // データ読み込み
        if (next_read_time <= now) {

            print_readlog("\n");
            print_readlog("Read data...\n");

            // ランダムな行数だけ読み込む
            int read_line_count = rand_engine() % (read_line_num_max - read_line_num_min + 1) + read_line_num_min;
            while (read_line_count > 0) {

                // リングバッファが満杯だったら終了する
                if (ring_buf.full()) {
                    print_readlog("RingBuffer is full.\n");
                    break;
                }

                // 末尾に到達していたら、先頭に戻す
                if (ifs.eof()) {
                    ifs.seekg(0, std::ios_base::beg);
                }

                // 1行読み込んで、リングバッファに格納
                std::string s {};
                std::getline(ifs, s);
                ring_buf.push_back(s);
                print_readlog((s + "\n").c_str());

                --read_line_count;
            }

            next_read_time += read_interval;
        }
        
        // データ書き出し
        if (next_write_time <= now) {

            std::cout << "\n";
            std::cout << "Write data...\n";

            // リングバッファから一定行数取り出して、標準出力に書き出す
            for (int i = 0; i < write_line_num; ++i) {
                if (ring_buf.empty()) {
                    std::cout << "empty.\n";
                }
                else {
                    auto s = ring_buf.front();
                    ring_buf.pop_front();
                    std::cout << s << "\n";
                }
            }

            next_write_time += write_interval;
        }

    }
}

読み込むファイルは、数十行程度あれば適当な内容のテキストで問題ありません。終端の行まで読み取ってしまったら、先頭に戻ってくるようにしています。ここでは次のようなものを用意しました。

aaaaa
bbbbb
ccccc
ddddd
eeeee
fffff
ggggg
hhhhh
iiiii
jjjjj
kkkkk
lllll
mmmmm
nnnnn
ooooo
ppppp
qqqqq
rrrrr
sssss
ttttt
uuuuu
vvvvv
wwwww
xxxxx
yyyyy
zzzzz

いくつか定義されている constexpr変数で、読み書きの時間間隔や行数を調整できます。

READ_LOG_ENABLEDマクロの置換結果を 1 にすると、読み取り時のログも出力されます。その状態で実行すると次のような出力結果になります。ランダムに揺らがせているので、毎回結果は異なります。

実行結果:


Read data...
aaaaa
bbbbb

Read data...
ccccc
ddddd

Read data...
eeeee

Write data...
aaaaa
bbbbb
ccccc
ddddd
eeeee
empty.

Read data...
fffff
ggggg
hhhhh

Read data...
iiiii

Read data...
jjjjj

Write data...
fffff
ggggg
hhhhh
iiiii
jjjjj
empty.

Read data...
kkkkk

Read data...
lllll
mmmmm
nnnnn

Read data...
ooooo
ppppp
qqqqq

Write data...
kkkkk
lllll
mmmmm
nnnnn
ooooo
ppppp

Read data...
rrrrr

Read data...
sssss
ttttt

Read data...
uuuuu
vvvvv
wwwww

Write data...
qqqqq
rrrrr
sssss
ttttt
uuuuu
vvvvv

Read data...
xxxxx
yyyyy

Read data...
zzzzz
aaaaa

Read data...
bbbbb

Write data...
wwwww
xxxxx
yyyyy
zzzzz
aaaaa
bbbbb

Read data...
ccccc
ddddd

Read data...
eeeee
fffff
ggggg

Read data...
hhhhh

Write data...
ccccc
ddddd
eeeee
fffff
ggggg
hhhhh

Read data...
iiiii
jjjjj

Read data...
kkkkk
lllll
mmmmm

Read data...
nnnnn
ooooo

Write data...
iiiii
jjjjj
kkkkk
lllll
mmmmm
nnnnn

Read data...
ppppp

Read data...
qqqqq
rrrrr
sssss

Read data...
ttttt
uuuuu
vvvvv

Write data...
ooooo
ppppp
qqqqq
rrrrr
sssss
ttttt

Read data...
wwwww

Read data...
xxxxx
yyyyy

Read data...
zzzzz
aaaaa

Write data...
uuuuu
vvvvv
wwwww
xxxxx
yyyyy
zzzzz

Read data...
bbbbb
ccccc
ddddd

Read data...
eeeee
fffff
ggggg

Read data...
hhhhh

Write data...
aaaaa
bbbbb
ccccc
ddddd
eeeee
fffff

読み込みのほうが遅れていて、予定していた量の出力ができない場合に empty. と出力されています。動画のストリーミング再生であれば、これが映像や音声が固まる原因となります。しかしそれでもプログラム自体が破綻することはなく、続きのデータがリングバッファに読み込まれれば、きちんとそこから出力が再開できます。

まとめ 🔗


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク 🔗


練習問題 🔗

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次のプログラムが出力する2つの値は true、false のどちらになりますか?

#include <chrono>
#include <iostream>

int main()
{
    std::chrono::duration<int, std::milli> d1 {1000};
    std::chrono::duration<int, std::micro> d2 {1000};

    std::cout << std::boolalpha << (d1 == d2) << "\n";
    std::cout << std::boolalpha << (d1.count() == d2.count()) << "\n";
}

解答・解説

問題2 (基本★)

標準出力から指定された秒数だけ待ったあと、“Hello” と出力するプログラムを作成してください。

解答・解説

問題3 (調査★★)

お使いの処理系で、system_clock、steady_clock、high_resolution_clock について、分解能がどれだけあるか、時間が逆行するかどうかを調べてみてください。

解答・解説


解答・解説ページの先頭



更新履歴 🔗




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る