Visual C++のエラーC1033、C2471の原因。

 Visual C++でプロジェクトをビルドしている時、稀に

C2471: プログラム データベース 'file' を更新できません。
C1033: プログラム データベース 'file' を開けません。

というようなエラーが出る場合がある。
 多くの場合、これはソースコードの誤りなどではなくストレージなどの問題である。私の場合Visual StudioのソリューションをOne Drive下に作成していることで稀にファイルの同期と競合してしまうのか、このエラーを時々見ている。大抵、One Driveの同期を一時停止すれば解決した。

 ただ今回はちょっと珍しいケースに遭遇した。プログラムデータベースというのは.pdbファイルのことで、各々のプロジェクトフォルダの中に作成されているはずだが、これの2GBというファイルサイズ上限に引っかかってしまったようなのだ。
 テンプレートを使って徹底的に仮想関数を廃したちょっとゴリ押しのプログラムを作ってみたところ、メモリの乏しいPCではメモリ不足でコンパイルできないほどの代物が出来上がってしまったのだが、メモリ44GBを搭載するPCでビルドしてもなお上記のエラーが出てしまい頭を抱えていたところ、.pdbファイルの大きさが2.16GBにまで巨大化していることに気がついた。プロジェクトの設定やバージョンにもよるだろうが、これは最大で2GBまでしか扱えないようだ。

 .pdbファイルとは要するにデバッグ情報を保持しているファイルのことだ。これはプロジェクトのプロパティで「C/C++->全般->デバッグ情報の形式」に「なし」以外のものを指定している時に生成される。
 ということは、もしデバッグ情報が一切不要であるのなら、これを「なし」にしてしまえばよい。ファイルそのものが生成されないのでエラーの起こりようがない。けれどもデバッグが不要なプロジェクトというのはなかなか存在しないので、可能なら別の方法を探したいところである。
 別の解決方法としては、プロジェクトを分割するという手がないわけではない。プロジェクトを分割すればそれに伴って.pdbファイルも分割されるので、恐らくビルドはできる。私が試した際は.pdbファイルとはまた別の問題が発生してしまったが。

多重ループを一つに纏める直積集合イテレータを作ってみた。

 Range-based for loopを理解してからというもの、その悪用を色々と思いついてしまって。
 今日も小ネタである。実用性はあんまりない。多重ループにすれば良いところを、敢えてそれを統合し一つのループで表現したくなってしまった病的C++erのお遊びだ。唯一メリットがあるとすれば、多重ループでは難しいcontinueによる全ループの脱出に対応するところか。

 次のような状況を考えてみたい。

std::vector<double> x;
std::vector<double> y;
std::vector<double> z;
//x, y, zには何らかの値が格納されているものとする。
for (auto x_ : x)
{
    for (auto y_ : y)
    {
        for (auto z_ : z)
        {
            //何らかの処理。
        }
    }
}

 ごく一般的な多重ループだ。訓練されたC++プログラマーであれば別段苦にはしないだろう。
 ただ、コードの可読性よりも見栄えの美しさに興味が湧いてきてしまった人間は、その不格好さが気になり始めるのだ。次のように書けないのだろうか。

for (auto [x_, y_, z_] : (x, y, zの積集合))
{
    //何らかの処理。
}

 というわけでちょっと作ってみた。

#include <tuple>
#include <vector>
#include <iostream>

namespace detail
{

template <class ...Iterators>
class ProductIterator
{
public:

    ProductIterator(std::tuple<Iterators...> begin, std::tuple<Iterators...> end)
        : mIterators(begin), mBegin(begin), mEnd(end)
    {}
    ProductIterator(std::tuple<Iterators...> begin, std::tuple<Iterators...> end, std::tuple<Iterators...> current)
        : mIterators(current), mBegin(begin), mEnd(end)
    {}
private:
    template <size_t Index, std::enable_if_t<(Index != 0), std::nullptr_t> = nullptr>
    ProductIterator& Increment()
    {
        auto& it = std::get<Index>(mIterators);
        ++it;
        if (it == std::get<Index>(mEnd))
        {
            it = std::get<Index>(mBegin);
            return Increment<Index - 1>();
        }
        return *this;
    }
    template <size_t Index, std::enable_if_t<(Index == 0), std::nullptr_t> = nullptr>
    ProductIterator& Increment()
    {
        ++std::get<0>(mIterators);
        return *this;
    }
public:
    ProductIterator& operator++()
    {
        return Increment<sizeof...(Iterators) - 1>();
    }
private:
    template <size_t ...Indices>
    auto GetContents(std::index_sequence<Indices...>) const noexcept
    {
        return std::forward_as_tuple(*std::get<Indices>(mIterators)...);
    }
public:
    auto operator*() const noexcept
    {
        return GetContents(std::make_index_sequence<sizeof...(Iterators)>());
    }
    bool operator==(const ProductIterator& it) const
    {
        return mIterators == it.mIterators;
    }
    bool operator!=(const ProductIterator& it) const
    {
        return !(*this == it);
    }

private:
    const std::tuple<Iterators...> mBegin;
    std::tuple<Iterators...> mIterators;
    const std::tuple<Iterators...> mEnd;
};

template <class ...Iterators>
auto MakeProductIterator(std::tuple<Iterators...> begin, std::tuple<Iterators...> end)
{
    return ProductIterator<Iterators...>(begin, end);
}
template <class ...Iterators>
auto MakeProductIterator(std::tuple<Iterators...> begin, std::tuple<Iterators...> end, std::tuple<Iterators...> current)
{
    return ProductIterator<Iterators...>(begin, end, current);
}

template <class Containers, class Indices>
class ProductRange;
template <class ...Containers, size_t ...Indices>
class ProductRange<std::tuple<Containers...>, std::index_sequence<0, Indices...>>
{
public:
    template <class ...C>
    ProductRange(C&& ...c)
        : mContainers(std::forward<C>(c)...)
    {}

    auto begin() const
    {
        auto beg = std::make_tuple(std::get<0>(mContainers).begin(), std::get<Indices>(mContainers).begin()...);
        auto end = std::make_tuple(std::get<0>(mContainers).end(), std::get<Indices>(mContainers).end()...);
        return MakeProductIterator(beg, end);
    }
    auto end() const
    {
        auto beg = std::make_tuple(std::get<0>(mContainers).begin(), std::get<Indices>(mContainers).begin()...);
        auto end = std::make_tuple(std::get<0>(mContainers).end(), std::get<Indices>(mContainers).end()...);
        auto cur = std::make_tuple(std::get<0>(mContainers).end(), std::get<Indices>(mContainers).begin()...);
        return MakeProductIterator(beg, end, cur);
    }
private:
    std::tuple<Containers...> mContainers;
};

}

template <class ...Containers>
auto MakeProductRange(Containers&& ...c)
{
    return detail::ProductRange<std::tuple<Containers...>, std::make_index_sequence<sizeof...(Containers)>>(std::forward<Containers>(c)...);
}

int main()
{
    std::vector<int> x{ 1, 2, 3 };
    std::vector<int> y{ 4, 5, 6 };
    std::vector<int> z{ 7, 8, 9 };

    for (auto [x_, y_, z_] : MakeProductRange(x, y, z))
    {
        printf("%d * %d * %d = %3d\n", x_, y_, z_, x_ * y_ * z_);
    }

    return 0;
}

 x、y、zの三重ループを巡回するのと同じような振る舞いをする特殊なイテレータを作っている。一応見た目はすっきりした。
 しかしあくまで疑似三重ループでしかないので、多重ループ中のbreakやcontinueのように一つのループのみを脱出するという動作に対応しない。不格好さを許せるのなら実装する手段はありそうだが、私にはどうせ使う機会がないので作らなかった。もしより多彩な機能を欲するのなら上のコードを適当に改造してほしい。

 私はC++だけでなくPythonも使うことがあるのだが、Visual StudioPythonのコードを編集しているとインデントによるブロックを調整するのがとても面倒くさく感じてくる。C++であれば{}を書き込むことで自動的に中身をフォーマットしてくれる。しかしPythonはそのような明確な区切り文字がないので、いちいち自分でインデントを調整しなければならない。もし既に書かれたコードの外側にループを追加したい場合、範囲内のブロック全てに手作業でインデントを追加しなければならなかったりする(選択範囲に一括でインデントを追加する機能はある)。とにかく面倒なのだ。スクリプト言語の“簡潔さ”というメリットがIDEとの相性の悪さによって死んでいる。
 せめて多重ループを何とかしたいと思い探してみると、Pythonにはitertools.productという関数があるらしいことを知った。直積のリストを生成するだけの雑な機能ではあるが、インデントを省略できる点はありがたかった。実行速度は悪化しそうな気もするがPythonを使う時点でお察しなので気にしていない。
 これをC++で再現してみようと考えたわけである。しかし高速化に狂っているC++er的には直積のstd::vectorを作るなんて外道な方法を採用するわけにはいかないので、上述のようなイテレータの形にしてみた。雑な方法だがまあ一応動作するし、お遊びとしては及第点ではなかろうか。

ムーブセマンティクスに対応するThreadPool。

 プログラミング界隈でよく知られたマルチスレッド処理のためのパターンであるスレッドプール。使用可能なスレッド数を予め定めておき、そこにキュー方式で処理を追加していくもの。走らせたい関数が100個くらいあったとしても、それを順番待ちに追加して逐次処理してくれる便利なものである。
 C++標準にはスレッドプールは存在しないのだが、思い思いのシングルヘッダー実装がGitHubなどで公開されているので、そのへんのどれかを適当に拾ってきて使えばいい。

 ただし残念なことに、一般公開されているそれらはやや問題を抱えている場合が多い。多くのものはstd::functionやstd::bind、ラムダ式などをそのまま使っていて、ムーブセマンティクスに対応しないかったり、場合によっては通常の参照渡しにさえ対応しないのだ。std::functionはnon-copyableな関数オブジェクトを持つことが出来ないしstd::bindはムーブセマンティクス非対応である。スレッドプールでムーブ不可って意味が分からんレベルの欠陥設計だと思うのだが、コピーコストの大きな変数をも値渡しさせる気なのか?コピー不可のインスタンスを渡したい場合はどうするんだ?そのあたりまでユーザーが頑張ってフォローしろというのか?

 そんなわけで、ムーブセマンティクスに対応するスレッドプールを実装した。これは私のもう一つのブログで公開しているそれの改良版である。あちらはちょっと仕様に気持ちの悪さがあったので、それを修正した形だ。C++11以上で動くはずだが、C++11限定の環境をすぐに用意できなかったので、MSVC2017のC++14環境でのみ動作確認している。昔のバージョンはMSVC2013で動作することを確認したものの、あれからかなり修正したので、C++11やGCC等ではコンパイルできないかもしれない。
 ソースコードはちょっと長いのでGitHubに置いておく。
github.com

使い方

 だいたい以下のように使う。

std::vector<int> func(std::vector<int>&& v)
{
    //スレッドプールに与える関数。
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::vector<int> s = std::move(v);
    std::sort(s.begin(), s.end());
    return std::move(s);
}
int main()
{
    std::mt19937_64 mt;
    std::uniform_int_distribution<> ui(0, 100);
    ThreadPool tp(4);//4スレッド分用意する。
    std::vector<std::future<std::vector<int>>> f;
    for (int i = 0; i < 20; ++i)
    {
        std::vector<int> v(10);
        for (auto& vv : v) vv = ui(mt);
        f.push_back(tp.AddTask(&func, std::move(v)));
    }
    for (int i = 0; i < 20; ++i)
    {
        auto result = f[i].get();
        for (auto v : result) printf("%2d ", v);
        printf("\n");
    }
    tp.Join();
}

 以下、必要な関数等。

ThreadPool(int num_of_threads);//コンストラクタ
std::future<result_of_func> AddTask(Func func, Args ...args);//関数ポインタ、関数オブジェクトの場合。
std::future<result_of_func> AddTask(MemFunc func, Ptr* ptr, Args ...args);//メンバ関数ポインタの場合。
void Join();//すべての処理が完了するまで待機。

 ThreadPoolのコンストラクタには用意したいスレッド数を与える。どれだけTaskを与えられたとしても、ここで与えたスレッド数を超えて同時に処理されることはなく、上限に達した場合は順番待ちに追加される。
 メンバ関数AddTaskによって処理を追加する。funcには関数ポインタ、関数オブジェクト、メンバ関数ポインタを与えることができる。argsにはその関数を呼び出すために必要な引数を、メンバ関数の場合は第2引数にそのメンバ関数を呼び出したいインスタンスへのポインタを与える。このあたりはINVOKEと似ている。
 もし引数を参照渡ししたい場合はstd::reference_wrapperを、moveしたい場合はstd::moveを使う。
 AddTask関数の戻り値は与えた関数の戻り値を受け取るためのstd::futureである。
 Join関数を呼ぶと、与えられたTaskが全て完了するまで待機する。JoinはThreadPoolインスタンスのデストラクタが勝手に呼ぶので、待機する必要がない場合は呼ばなくともよい。

簡単な解説

 前半200行くらいで長たらしく書かれているのは、C++11に存在しないstd::applyやstd::invokeの代用品、std::reference_wrapperを引っ剥がすものなどである。理解できないのなら読み飛ばしてよい。  ThreadPoolを実装する方法そのものは何通りか考えられる。私が初めて実装したときは、AddTask関数によって処理が追加されるたびに新しいスレッドを作成するといういい加減なものだった。スレッドの作成はそれなりにコストが大きいので、可能であれば使い回すほうが良い。
 今回は事前に用意しておいたWorkerThreadが各々ThreadPoolの持つキュー(std::deque<std::function<void()>> mQueue)へと処理を受け取りに行く構造になっている。まあ大抵のThreadPoolはこのような設計になっているだろう。
 しかし上述のように、std::functionとstd::bindをそのまま使うと問題が発生する。ムーブセマンティクスに対応するためには、キューに一次格納しておく関数オブジェクトを

  • std::bindを用いることなく、
  • non-copyableな引数を保持している場合にもcopy-constructibleに偽装する

ことが必要だった。よってstd::bindではなくTaskBinderクラスを作成し、こちらに引数を束縛するよう設計した。TaskBinderクラスは呼ばれると例外を投げるだけのコピーコンストラクタを持つため、コンパイラ的にはcopy-constructibleである。実際にはムーブコンストラクタが定義されている限りコピーコンストラクタが呼ばれることはないため、このような設計で問題ない。

 というわけで、std::functionとstd::bindの伏線回収を完了した。いやまあ、C++的にそんなに複雑な話でもないのだが、基礎知識に乏しかった頃はこのあたりの意味わからん仕様に苦しめられて、上のようなThreadPoolを正常動作させるために何日も浪費したのだ。三回分の記事にするくらいの苦労はしていると思う。これらの記事が、同じように苦労している人たちの助けとなってくれれば幸いだ。