Chapter 05

便利なライブラリ機能

dec9ue
dec9ue
2021.12.18に更新

ここまで来ると言語機能的にはもう相当にシャレオツなのですが、その状態でC言語も使わないのに #include <string.h> とかしてしまうともう本当に見てられないので、こういうライブラリ機能を使えばいいよ!というのをピックアップして紹介してみます。

この辺はもう、タイトルをみて気になったものを眺めてもらうだけでいいと思います。

コンテナ

動的にサイズが決定するような配列はほとんど使わず、std::vector を使うのが基本なので、これで運用していきましょう。想定する大きさがあれば std::vector::reserve() で予め大きめの領域を確保して作業しましょう。こうすることでリアロケーションによる実行コストを回避できます。

std::vector<int> v;

v.reserve(10);

for(int i=0;i<5;i++)
{
    v.push_back(i);
}

集合やハッシュマップの概念は std::set / std::map があります。もう説明するだけ無駄かも知れませんが、順序や重複可否などのバリエーションを静的に設定できるのがC++の良さでもあります。<set> / <map> あたりの仕様を確認してから使い始めると良いでしょう。

範囲forも使えて便利です。

#include <set>
#include <iostream>

std::set<int> s;

void print_the_set()
{
    for(const auto& item : s)
    {
        std::cout << "item : " << item << std::endl;
    }
}

bool has_a_member(int item)
{
    // return s.contains(item); // これはc++20以降
    return s.find(item) != s.end();
}

map についても範囲for文が使えますがこれについては<<_pair_tuple>>のセクションを見てください。

pair / tuple

2つ、あるいは3つ以上のデータをセットで扱う仕組みです。他のプログラミング言語では当たり前にあったものがC++には長く存在しませんでした。 std::make_pair / std::make_tuple を使用して構築したり、型推論が効いているところでは初期化リスト {} を使って構築することも可能です。

#include <tuple>

std::pair<int,std::string> p1{2,"aaa"};

auto p2 = std::make_pair(2,"aaa");

std::pair<int,std::string> get_p3()
{
    return {2,"aaa"};
}

std::tuple<int,std::string,std::string> t{3,"aaa","bbb"};

auto t2 = std::make_tuple(3,"aaa","bbb");

std::tuple<int,std::string,std::string> get_t3()
{
    return {3,"aaa","bbb"};
}

C++17から pair / tuple を簡単に分解する言語機構が使えるようになりました。構造化束縛といいます。値としての束縛も、参照としての束縛もできます。

auto [k1,v11] = get_p3(); // 値として束縛

auto t3 = get_t3(); 
auto& [k2,v21,v22] = t3; // 参照として束縛

構造化束縛によって map の範囲for文が書きやすくなりました。

std::map<int,std::string> a_map {
    {3,"aaa"},{2,"bbb"}
};

for(auto const& [key, value] : a_map)
{
    std::cout << "key is : " << key << " value is : " << value << std::endl;
}

構造化束縛によって、やっと pair / tuple を便利に使える土台ができたという感じがします。ただし、構造化束縛は今のところネストできません。また、値で束縛するか参照で束縛するかを項目ごとに選ぶこともできません。

文字列

このガイドの執筆時点での文字列の標準的な型は std::string つまり std::basic_string<char> です。なので、当面はこれを基本的な文字列の型として使用することになるでしょう。(Windows環境だと std::wstring が標準になりますが、、、)

C++20以降は std::u8string つまり std::basic_string<char8_t> が主流になっていくものと思われます。これらは依存ライブラリなどの互換性を考慮して決めていくことになると思われます。[1]

また、C++17で部分文字列など、リソースをシェアした文字列の考え方として std::string_view が導入されています。部分文字列を引き回したりする場合には std::string& を使うより、std::string_view を使うほうが便利かも知れません。また、 std::string& から std::string_view には暗黙のキャストがあるので、関数のインターフェースに std::string_view を使用すると std::string でも std::string_view でも使用できるようになります。

std::string s1 = "string";
char s2[] = "char array";

void print(std::string_view sv)
{
    // coutはstring_viewを受け付けるのでこれ自体は意味ないが
    std::cout << sv << std::endl;
}

int main()
{
    print(s1);
    print(std::string_view(s1));
    print(s2);
}

なお、string_view についてもC++20以降は std::string 同様に std::u8string_view に移行していくものと思われます。

正規表現

正規表現が標準化され、比較的便利に使用できるようになっています。正規表現は文字列から作りますが、生文字列リテラルを使用したほうが読みやすくなるため、生文字列リテラルを使用することが多いようです。

#include <iostream>
#include <regex>

int main()
{
    const char s[] = R"(She said "He is hungry.")";
    const std::regex re(R"("(\w+) is (\w+).")");

    std::cmatch m;
    if (std::regex_search(s, m, re))
    {
        for (std::size_t i = 0, n = m.size(); i < n; ++i)
        {
            std::cout << i << ":'" << m.str(i) << "\', "
                << "position = " << m.position(i) << ", " 
                << "length = " << m.length(i) << std::endl;
        }
    }
}

.結果

0:'"He is hungry."', position = 9, length = 15
1:'He', position = 10, length = 2
2:'hungry', position = 16, length = 6

ただし、2021年時点においても、非ASCII文字に対しては標準ライブラリの実装はロクに動かないことが多いようです。筆者も SRELL[2] という互換ライブラリを使用することが多いです。

SRELLのサイトのサンプルをそのまま引用します。ほぼ、標準のregexと使い方が同じだとわかると思います。

.SRELLの使い方

//  Example 01:
#include <cstdio>
#include <string>
#include <iostream>
#include "srell.hpp"

int main()
{
    srell::regex e;     //  正規表現オブジェクト。
    srell::cmatch m;    //  結果を納めるオブジェクト。

    e = "\\d+[^-\\d]+"; //  正規表現をコンパイル。
    if (srell::regex_search("1234-5678-90ab-cdef", m, e))
    {
        //  printfを使うなら。
        const std::string s(m[0].first, m[0].second);
            //  上は下のどちらかでも良い。
            //  const std::string s(m[0].str());
            //  const std::string s(m.str(0));
        std::printf("result: %s\n", s.c_str());

        //  iostreamを使うなら。
        std::cout << "result: " << m[0] << std::endl;
    }
    return 0;
}

std::u8string が当たり前になった世界では標準ライブラリの正規表現もきちんと動くようになると思われます。

時刻

<chrono> ヘッダのライブラリを使用すると現在時刻や時刻差分が扱えます。プラットフォーム非依存で時刻取得が行えます。一般的な時刻管理の用途であればこれで十分でしょう。

#include <chrono>
#include <iostream>

// この一行を書くのにちょっと憂鬱になるというのは内緒
using std::literals::chrono_literals::operator"" h;

int main()
{
    auto now = std::chrono::system_clock::now();
    auto after_4_5 = std::chrono::time_point_cast<std::chrono::minutes>(now + 4.5h);

    std::time_t t0 = std::chrono::system_clock::to_time_t(now);
    std::cout << "current time is " << std::ctime(&t0) << std::endl;
    std::time_t t1 = std::chrono::system_clock::to_time_t(after_4_5);
    std::cout << "after 4.5h'll be " << std::ctime(&t1) << std::endl;
}

.結果

current time is Wed Mar 10 23:05:44 2021

after 4.5h'll be Thu Mar 11 03:35:00 2021

ファイルシステム

ファイルシステムを取り扱う標準ライブラリ機能はこれまで貧弱、というかありませんでした。C++17以降、std::filesystem が導入されて、ディレクトリ操作も含めた互換性の高いファイルシステム操作が行えるようになりました。これはクロスプラットフォームで開発するときにはとてもありがたい機構で、これまでPOSIXやWindowsのAPIラッパを書いていた方々がメンテナンスから開放される日も近いのではと思います。

#include <iostream>
#include <filesystem>
#include <fstream>

namespace fs = std::filesystem;

int main()
{
    const std::string dir0("dir0"), dir1("dir1");
    const std::string file0("file0.txt"), file1("file1.txt");
    constexpr auto& SEP = fs::path::preferred_separator;

    // dir0/file0.txt を作成
    fs::create_directory(dir0);
    std::ofstream(dir0 + SEP + file0);
    // dir0/dir1/file1.txt を作成
    fs::create_directory(dir0 + SEP + dir1);
    std::ofstream(dir0 + SEP + dir1 + SEP + file1);

    // ディレクトリ内をイテレータで走査する (※型定義はautoだが、明示するためにあえて記述)
    for (const fs::directory_entry& x : fs::directory_iterator(dir0)) {
        if(x.is_regular_file())
        {
            std::cout << x.path() << " is a regular file." << std::endl;
        }
        else if(x.is_directory())
        {
            std::cout << x.path() << " is a directory." << std::endl;
        }
        else
        {
            std::cout << x.path() << " is an unknown entry." << std::endl;
        }
    }
}

.結果

"dir0/file0.txt" is a regular file.
"dir0/dir1" is a directory.

コンパイラのバージョンによっては experimental の名前空間に入っているのでエイリアスしてあげる必要があることがあります。

#include <experimental/filesystem>

namespace fs = std::experimental::filesystem;

if(fs::create_directory("new_directory") != true){
    // ....
}

async

非同期処理で計算結果を得たいなど、ワンショットで処理を非同期に出したいだけであれば std::async が便利です。いわゆる「Futureパターン」を提供します。

結果を取得する際に std::future::get() を使用しますが、参照型と実体型のいずれも使用できます。実体型ではムーブが優先されます。(*)

#include <future>

int main()
{
    MovableObj1 m1;
    // fの型は std::future<MovableObj2>
    auto f = std::async( std::launch::async, [m1 = std::move(m1)](){
        MovableObj2 m2;
        // なにかMovableObj(m1)を使った処理
        return m2;
    });
    auto& mr2 = f.get(); // ちなみにmr2の型はMovableObj2&
    // auto m2 = f.get(); // ムーブ/コピー可能であれば値で受けることも可能。
    // ただし、future::get()が呼べるのはひとつのオブジェクトあたり一回のみ
}

(*) ムーブが「優先」というのは std::future::get() の戻り値が std::move() が修飾されているのと同等、という意味です。ムーブがあればムーブで、コピーがあればコピーで返され、どちらもなければコンパイルエラーになります。この辺の感覚は普通の戻り値に近いですが、関数の戻り値は一度 future によって確保され、その後受け渡されることを頭に入れておくと良いと思われます。

mutex

かなり説明を省略するのでこのセクションはPOSIXのpthreadの概念に慣れていないと意味不明かも知れません。興味のない人は読み飛ばしてもらってOKです。

排他処理と条件変数を実現する機能として std::thread / std::mutex / std::condition_variable があります。pthreadに相当する機能です。C++ではmutexロックの取得と開放をスコープと紐付けられるので開放漏れにビクビクする必要がありません。

#include <thread>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <optional>

class SynchronizedData {
    private:
    std::mutex m;
    std::condition_variable cv;

    bool data_produced = false;
    std::optional<MovableObj> mo = std::nullopt;

    public:
    void produce()
    {
        // データの準備処理
        mo = std::make_optional<MovableObj>();

        // mutexでロックを実施(左辺値のスコープ切れと同時にロック開放)
        std::unique_lock<std::mutex> lock(m);
        data_produced = true;
        cv.notify_all();
    }

    void consume()
    {
        // mutexでロックを実施(左辺値のスコープ切れと同時にロック開放)
        std::unique_lock<std::mutex> lock(m);
        // 条件変数でデータの準備ができるまで待機
        cv.wait(lock, [this] { return data_produced; });

        // データ処理の実施
        // mo->...
    }
};

int main()
{
    SynchronizedData d;

    std::thread t1([&] { d.consume(); });
    std::thread t2([&] { d.produce(); });

    t1.join();
    t2.join();

    return 0;
}

なお、これだけの例を挙げておいてなんですが、そもそも、この例に挙げているくらいのことしかやらないのであれば前のセクションで紹介した future の適用を考えるべきです。プロデューサー側のスレッドは std::async で生成し、コンシューマー側のスレッドに future をムーブ渡しすればいいのです。std::mutex とか std::condition_variable のことを考える必要はありません。

#include <future>

MovableObj produce()
{
    MovableObj m;
    // データの準備処理
    return m;
}

void consume(MovableObj& m)
{
    // データ処理の実施
}

int main()
{
    SynchronizedData d;

    auto f1 = std::async(std::launch::async, [] { return produce(); });
    auto f2 = std::async(std::launch::async, [f=std::move(f)] { consume(f.get()); });

    // f1はムーブ済みのため、f2のラムダの終了に伴って回収される
    f2.wait();
    // f2はmainのスコープ切れによって回収される
    return 0;
}
脚注
  1. ところで、<iostream>std::cout はC++20においても std::u8string に対応していないそうです。標準ライブラリにはない、文字列変換ライブラリを通してから std::cout に渡しなさいということだそうです。なんだそれ。30年以上にわたる非欧米系文字圏のみなさんの文字コードに関する怒りが、C++標準化委員会にはまだ完全には通じていないようです。 ↩︎

  2. URL http://www.akenotsuki.com/misc/srell/ ↩︎