あめだまふぁくとりー

Boost.Graphとかできますん

名前解決から始める C++

この記事は 初心者 C++er Advent Calendar 2015 14 日目の記事です.

いきなりですが次のコードを見てください.

#include <iostream>

struct cat
{
  std::string name;
};

void print_name(cat const& neko) // const 参照渡し!
{
  std::cout << "this cat name is " << neko.name << std::endl;
}

int main()
{
  cat neko{"katsuo"}; // 波括弧初期化!!

  print_name(neko);
}

const 参照渡しと波括弧初期化はそれぞれ 8 日目2 日目の記事で説明があったので, ここまでの記事を読んだ人なら上記のコードはわかるはずです (struct cat をちゃんとしたクラスにしようと思いましたけど長くなるのでやめました. クラスについては 5 日目の記事を参照しくてださい).

ではここで問題です. このコードを実行すると何が起きるでしょうか?

さっそく回答です.

this cat name is katsuo

どうですか? 難しかったでしょうか?

問題 2

それでは続いて次のコードです.

#include <iostream>

namespace animals { // 名前空間

  struct cat
  {
    std::string name;
  };

  void print_name(cat const& neko) // const 参照渡し!!
  {
    std::cout << "this cat name is " << neko.name << std::endl;
  }

} // namespace animals

int main()
{
  animals::cat neko{"yamada"}; // 波括弧初期化!!!!

  print_name(neko); // おや? この呼び出しは?
}

struct catvoid print_name(cat const&)animals という名前空間に入れました.

それではこのコードを実行すると何が起きるでしょうか? と, その前に名前空間について少しだけ説明しましょう.

名前空間とは?

変数や関数等の名前をまとめる機構です. クラスでも名前をまとめることができますが, クラスの場合はそのクラスの役割に必要なものだけをまとめるのが普通です.

一方, 名前空間はもっと緩い意味で名前をまとめます. 例えば, 標準ライブラリの std のように特定のライブラリをまとめたものだったり, 関数の内部詳細をまとめたものだったりします.

名前空間の中と外ではスコープが違うので同じ名前のエンティティを定義できます.

int g; // グローバル変数の定義

namespace ns {

  int g; // スコープが違うので同じ名前の変数を定義できる

}

名前空間の中の名前を参照する場合はスコープ解決演算子 :: を使用します. これは <名前空間の名前>::<参照されるエンティティの名前> のように書きます.

namespace ns {

  void f(int i) {}

}

int main()
{
  f(3); // コンパイルエラー! X(
  ns::f(3); // OK! :)
}

問題 2 のコードでも main 関数から struct cat を参照する場合は animals::cat とスコープ解決演算子を使用していますね.

コードの続き

名前空間がなんとなくわかったところで先ほどのコードの続きです.

int main()
{
  animals::cat neko{"yamada"}; // 波括弧初期化!!!!

  print_name(neko); // おや? この呼び出しは?
}

このコードを実行すると何が起きるでしょうか? そもそもコンパイルできるのでしょうか? 名前空間を理解したので楽勝ですね. 試してみましょう.

melpon.org

どうですか? 期待通りの答えだったでしょうか?



いろいろ言いたいことはあるかもしれませんが次に進みます.

問題 3

#include <iostream>

int main()
{
  std::cout << 'A';
}

今度のコードはとてもシンプルですね.

このコードを見た人は「答えは A だ」と即答するかもしれません.


残念違います.


誰もこのコードを実行したら何が起こるかなんて聞いていません. 焦りは禁物です.

質問はこうです.

「このコード上に現れる << とは何か」.

あ, 哲学的に考える必要はありませんよ.

演算子オーバロード

std::coutstd::ostream 型を持つオブジェクトです. これにちなんで << をストリーム演算子と呼ぶ書籍もあったりします (多分).

というわけで <<演算子っぽいです. これはかなりいい線いっています. std::cout'A' を整数に置き換えてみましょう.

int main()
{
  8 << 2;
}

あ, これシフト演算子.

正確には上記コードの << は右シフト演算子演算子オーバロードした関数です.

この関数は以下のように定義されています (いろいろ正確でない部分がありますがここではこれで十分です).

namespace std {

  ostream cout;

  std::ostream& operator<<(std::ostream& os, char c)
  {
    // ... output c to stdout
  }

}

演算子 @ の使用は operator@ の呼び出しに置き換えられます (ここで @ は何かの演算子).

問題 4

上記のオーバロード演算子の定義と質問のコードとつなげてみましょう.

namespace std {

  ostream cout;

  std::ostream& operator<<(std::ostream& os, char c)
  {
    // ... output c to stdout
  }

}

int main()
{
  std::cout << 'A';
}

そしてシフト演算子を置き換えると関数の呼び出しに置き換えると...

namespace std {

  ostream cout;

  std::ostream& operator<<(std::ostream& os, char c)
  {
    // ... output c to stdout
  }

}

int main()
{
  operator<<(std::cout, 'A'); // 置き換え!
}

どことなく問題 2 のコードに似ていますね. それでは最後の問題です.

「問題 3 のコードでなぜ 'A' が出力されると思った?」

置き換え後は, std::operator<<(std::cout, 'A') ではなく operator<<(std::cout, 'A') になります. なぜなら, << のどこにも std なんて付いていないのですから.

このコードはまちがいなく問題 3 のコードと等価ですが, 名前空間の修飾なしに関数呼び出しを行っています.

つまり問題 2 のコードがコンパイルエラーになると思うのなら, 問題 3 のコードもコンパイルエラーになると思うべきなのです.

ADL (Argument-Dependent name Lookup)

この謎を解くのが ADL です.

ADL とは簡単にいうと, 引数の型と同じ名前空間にある関数も呼び出しの対象になりますよというものです.

問題 2 のコードでは print_name の引数の型は animals::cat です. なので ADL によって名前空間 animals 内の print_name も呼び出し対象に含まれます.

問題 4 のコードでは引数の型は std::ostreamchar でした. よって ADL によって名前空間 std 内の operator<< も呼び出し対象になるのです.

これが上記二つのコードがコンパイルエラーにならず実行できた理由です.

ちなみに組み込み型には名前空間がないので char 型の引数は特に影響しません.

また, ADL が行われるのは関数の名前にスコープ解決演算子が付いてないときだけです.

namespace animals {

  struct cat;

  void print_name(cat const& neko);

}

namespace ns {

  void print_name(int a); // 引数の型は int

}

void print_name(int a); // 引数の型は int

int main()
{
  animals::cat neko{"norisuke"};

  ns::print_name(neko); // コンパイルエラー!! ADL なし

  ::print_name(neko); // コンパイルエラー!! この場合も ADL なし

  print_name(neko); // OK! ADL あり
                    // 引数の型から ::print_name ではなく
                    // animals::print_name を呼び出す
}

このように C++ では一見コンパイルエラーに見える関数呼び出しも ADL によって問題なく実行できる場合があります.

問題 4 で示した通り, 演算子オーバロードを自然な形で呼び出すことができるのは ADL のおかげなのです.

ADL によるアルゴリズムのカスタマイズ

ここからは中級者への一歩. ADL を使ったアルゴリズムのカスタマイズを見ていきます.

以下のコードを見てください.

namespace others_libs {

  // 座標取得関数
  template <class Vec>
  double get_x(Vec const& v) { return v[0]; }
  template <class Vec>
  double get_y(Vec const& v) { return v[1]; }

  // 二つのベクトルのなす角を計算
  template <class Vec>
  double angle(Vec const& v1, Vec const& v2)
  {
    double const x1 = get_x(v1);
    double const y1 = get_y(v1);
    double const length1 = std::sqrt(x1 * x1 + y1 * y1);

    double const x2 = get_x(v2);
    double const y2 = get_y(v2);
    double const length2 = std::sqrt(x2 * x2 + y2 * y2);

    return std::acos(
      (x1 * x2 + y1 * y2) / (length1 * length2));
  }

}

これは誰かが提供してくれた二つのベクトルのなす角を計算するライブラリです.

このライブラリではベクトルの各座標は配列のようにインデックスでアクセスできることを想定してます. なので, ベクトルを表現する型が配列や std::vector などインデックスアクセスがサポートされる任意の型で使用可能です.

ベクトルといえば 5 日目 の記事で Vec2 クラスを定義しました.

namespace my_libs {

    struct Vec2
    {
        double x;
        double y;

        // ... そのほかの関数定義
    };

}

ここでは話を進めやすくするために Vec2my_libs という名前空間の中に入れました.

ベクトルを使用するなら配列よりも, ベクトルとしてきちんと定義した型を使用したいですね. しかしながら, Vec2 はインデックスアクセスをサポートしていないため others_libs::angle と組み合わせることができません.

Vec2 をインデックスアクセスできるように変更すべきなのでしょうか?

いいえ. この問題を ADL が解決してくれます.

namespace my_libs {

    struct Vec2
    {
        double x;
        double y;

        // ... そのほかの関数定義
    };

}

namespace my_libs {

    // Vec2 と同じ名前空間に定義
    double get_x(Vec2 const& vec) { return vec.x; }
    double get_y(Vec2 const& vec) { return vec.y; }

}

Vec2 と同じ名前空間に関数を追加するだけで others_libs::angleVec2 に対して使用するようにできました *1.

この方法の凄いところは, Vec2 にも others_libs::angle にも一切変更を加えていないということです. つまり, どちらのソースも変更できない場合でもこの方法は適用可能なのです.

アダプタ関数

ここで注目すべきなのは others_libs::angle 中の get_xget_yアダプタとして機能していることです.

これらの関数の呼び出しではスコープ解決演算子を使用していません. つまり, ADL を利用してカスタマイズすることを想定しているのです *2.

このように, ADL はアルゴリズムのカスタマイズポイントとして利用ができます.

この ADL によるカスタマイズポイントは C++11 以降の範囲 for ループや STL, Boost ライブラリ等広く利用されています. そしてその際たる例が, 拡張可能なグラフライブラリとして設計された Boost.Graph なのです!! (本記事の目的達成).

まとめ

本記事ではもともとは shared_ptr のコストについて書く予定だったのですが全然初心者っぽくなかったので, Boost.Graph の宣伝も兼ねて ADL の説明をしました.

ADL にはプラスの面だけでなくマイナスの面もありますが, それは誰かが説明してくれるでしょう.

ADL についてより理解を深めたい方は Exceptional C++ の第 5 章も読むとよいと思います *3... と思ったらこの本絶版? 丸善さん再出版お願いしますー.

Exceptional C++―47のクイズ形式によるプログラム問題と解法 (C++ in‐Depth Series)

Exceptional C++―47のクイズ形式によるプログラム問題と解法 (C++ in‐Depth Series)

  • 作者: ハーブサッター,浜田光之,Harb Sutter,浜田真理
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2000/11
  • メディア: 単行本
  • 購入: 9人 クリック: 134回
  • この商品を含むブログ (63件) を見る

追記: 上記商品紹介中の Herb Sutter 氏の英語スペルが Harb Sutter になっていますが, 正しくは Herb Sutter です (@kariya_mitsuru さんご指摘ありがとうございます!!).


この記事は 初心者 C++er Advent Calendar 2015 14 日目の記事でした.

*1:Vec2 がグローバル名前空間に定義されている場合, 同様にグローバル名前空間に関数を追加すれば ADL が機能します.

*2:逆にカスタマイズを想定しない場合は名前空間で関数名を修飾すべきです.

*3:この本では ADL は Koenig の自動照合と呼ばれています

io_service の使い方

この記事は C++ Advent Calendar 2015 14 日目の記事です.

13 日目は okdshin さんの Boost.Computeでグラボを燃やす - クリアボックス でした.

本記事の内容

本記事では io_service をどう使うかについて書きます. 具体的には, io_service の使用モデルを整理, 比較して, それぞれのモデルを選択する上での指標を示します.

io_service の使用モデルについては以下の Christopher Kohlhoff 氏の発表内容でも触れられていますので こちらも見てみることをお勧めします.

なお, LinuxMac でしか調査していませんので Windows 等を使用している方は 本記事の内容は参考程度にとどめておいてください.

io_service の使用モデル

io_service の使用モデルは以下の四つに大別できます.

  • 単一スレッドで単一の io_service を使用する.
  • 複数のスレッドで io_service を共有する.
  • スレッドごとに io_service を用意する.
  • 複数のスレッドで複数の io_service を使用する.

以降の節ではこれらのモデルを簡単に説明していきます. なお最後のモデルは真ん中二つのモデルの組み合わせなので本記事では省略します.

シングルスレッドモデル

すべてのはじまり.

asio::io_service io_service{1};
io_service.run();

このモデルでは data race 等を意識する必要がないので他のモデルに比べてコードを単純にできます *1.

また, io_service のコンストラクタconcurrency_hint を 1 に指定することで, シングルスレッドでは不要な排他処理を減らしてパフォーマンスを少しだけ向上させることができます *2.

時間のかかる処理

このモデルで注意すべきことは各ハンドラの実行時間を短くすることです. 重い処理はワーカースレッドを用意してそちらで実行しましょう.

Christopher Kohlhoff 氏の発表でも紹介されていますが, これには asio::io_service::work が使用できます.

void handler()
{
    auto work = asio::io_service::work{io_service};
    // work を保持させる
    worker_thread.post([work]{
        long_running_task(); // 何か時間がかかる処理
        work.get_io_service().post(next_task);
    });

    // io_service のキューが空でも work が存在するので
    // io_service::run は終了せず次のハンドラ (next_task) を待ち続ける.
}

スレッドプールモデル

このモデルで使用する io_service はひとつです. このインスタンスメンバ関数 io_service::run をスレッドプール中のスレッドが呼び出します.

asio::io_service io_service{};
std::vector<std::thread> thread_pool{};
for (auto i = std::size_t{0}; i < nthreads; ++i) {
    thread_pool.emplace_back([&io_service]{
        io_service.run(); // invoke run for each thread
    });
}

このモデルは Boost.Asio の Strand のチュートリアル にも載っているので, 見たことある方も多いのではないかと思います.

このモデルの利点として, シングルスレッドモデルに比べてハンドラの実行時間を意識する必要がないことが挙げられます. ハンドラの処理が少し重くても後続のハンドラは待たされることなく別スレッドで実行されます.

また, スレッドプールのスレッド数を 1 にした場合, io_service::run が別スレッドで実行されるという点を除けばシングルスレッドモデルと完全に一致します. 逆に言うと, シングルスレッドモデルからこのモデルへの移行は容易ということです.

一方, 基本的に Asio で提供される I/O Object はスレッドセーフではないため, Strand または Mutex を使用してスレッド間の同期を行う必要があります.

Mutex による I/O Object のガード

I/O Object は asio::async_write 等の composed operation が存在するので 普通に I/O Object の使用の前後で Mutex を Lock/Unlock するだけでは不十分です.

Mutex で I/O Object を保護する例は Christopher Kohlhoff 氏の発表で紹介されています (https://github.com/boostcon/2011_presentations/raw/master/mon/thinking_asynchronously.pdf の pp.95-98).

氏の発表では std::mutex などの通常の Mutex を使用する例が紹介していましたが, 本記事では再帰的な Lock が可能な Recursive Mutex の使用をお勧めします *3.

なぜ Recursive Mutex なのかというと, Asio のハンドラ呼び出し機構の都合上, 再帰的な Lock を避けられないケースが存在するからです.

しかし Strand を使用した方が手間もなくスレッドのブロックも抑えることができるので, 基本的に I/O Object と Mutex を組み合わせる必要はないです *4.

io_service per スレッドモデル

このモデルではスレッド毎にひとつの io_service が存在します.

std::vector<asio::io_service> io_service_pool(nio_services);
std::vector<std::thread> threads{};
for (auto& io_service : io_service_pool) {
    threads.emplace_back[&io_service]{ // bind each io_service
        io_service.run();
    }
}

このモデルの場合, 各 I/O Object はひとつのスレッドに属することになるので, 基本的に Strand 等を使用した同期は必要ありません *5.

シングルスレッドモデルの説明で上がった concurrency_hint も適用できますし, io_service の数 (スレッド数) を CPU 数に合わせればパフォーマンス的にはスレッドプールモデルよりも良さそうです.

一方, シングルスレッドモデルと同様にハンドラの実行時間に注意する必要があります.

スレッドプールモデル vs io_service per スレッドモデル

ライブラリ / フレームワークの比較

スレッドプールモデルと io_service per スレッドモデルはどちらもマルチスレッドを組み合わせたモデルですが, どちらを採用すべきなのでしょうか?

Boost.Asio を使用しているライブラリ / フレームワークの実装を少し調べてみました.

なお本記事の調査では, Strand を使用している == スレッドプールで使用する意思があるとしています (Strand の使用状況に加えて, io_service::run を複数スレッドで呼び出せるかはチェックしています). しかし, Strand を正しく使用していない可能性があるので, これらのライブラリを使用する際は各自でドキュメントや実装を参照してください.

cpp-netlib 同期版サーバ

cpp-netlib/cpp-netlib · GitHub

単一の io_service を使用しています. Strand を内部で使用しているので*6, スレッドプールモデルで使用可能です.

cpp-netlib 非同期版サーバ

I/O に単一の io_service, ハンドラの実行に別の io_service をスレッドプールで使用しています (いわゆる Half-Sync/Half-Aync パターン). Strand を内部で使用しているのでスレッドプールモデルで使用可能です.

ただし, 書き込み処理にバグがありますし, ちゃんと使い方を知らないと使うのは難しい感じがしました.

WebSocket++

zaphoyd/websocketpp · GitHub

単一の io_service を使用しています. 設定次第では Strand を内部で使用するのでスレッドプールモデルで使用可能です.

AZMQ Boost Asio + ZeroMQ

zeromq/azmq · GitHub

単に I/O Object を提供しているだけなので任意のモデルでも使用可能 (のはず) です. 実装見た感じシングルスレッドで使用した方が幸せそうな感じがしました.

Cinder-Asio

BanTheRewind/Cinder-Asio · GitHub

単一の io_service を使用しています. Strand を内部で使用しているのでスレッドプールモデルで使用可能です. 多分 socket の書き込み処理がバグっています.

Simple-Web-Server

eidheim/Simple-Web-Server · GitHub

スレッドプールモデルを使用しています.

Boost.HTTP

BoostGSoC14/boost.http · GitHub

単に I/O Object を提供しているライブラリです. のくせに, Strand と組み合わせることもできないのでシングルスレッド相当でしか使えません *7.

nghttp2 - libnghttp2_asio

tatsuhiro-t/nghttp2 · GitHub

io_service-per-スレッドモデル を使用しています.

性能評価

こうやって見てみるとスレッドプールしか意識していないライブラリ / フレームワークが圧倒的に多いです.

多くのライブラリ / フレームワークがスレッドプールモデルを選択しているなら, スレッドプールを選択すればよいと考えるかもしれません.

が, その前に両モデルのパフォーマンスを計測してみてましょう. 本記事ではいくつかのケースで両モデルのパフォーマンスを計測してみました.

以下の計測には, 物理マシン Macbook (Early 2015), VirtualBox 上の Ubuntu 14.04 LTS (メモリ 1G, CPU x 4) を使用しました. コンパイラは clang 3.6, -std=c++11 -stdlib=libc++ -pedantic -O3 -DNDEBUG をオプションに指定しました.

スレッド数と 1 ハンドラあたりの実行時間の関係

このテストケースではあらかじめ io_service に 1,000,000 個のハンドラを登録し, io_service::run が終了するまでの時間を計測します. io_service-per-スレッドモデルの場合は合計 1,000,000 個のハンドラを各 io_service に均等に割り振っています.

計測に使用したコードは こちら になります.

ハンドラの実行時間を調整するためにハンドラ内で空ループを回しています.

空ループ数が 1,000 の結果が以下になります.

f:id:amedama41:20151125215044p:plain

io_service-per-スレッドモデルではスレッド数に対してスケールしていますが (スレッド数 3 以上が微妙ですが), スレッドプールモデルではスレッド数を 3 にすると遅くなっています.

今度は空ループ数が 5,000 の場合の結果を見てみます.

f:id:amedama41:20151125215059p:plain

どちらのモデルもほぼ同じ結果になりました.

スレッドプールモデルのこの結果は, io_service 上でのスレッド間の同期によるものと考えられます.

ハンドラが軽い場合は時間あたりの io_service へのアクセス時間の割合が大きくなるためスレッド間の競合が頻発します. ハンドラを遅くすると io_service へのアクセス時間よりもハンドラ処理の時間が全体の時間を占めるため, 競合が減って同期コストが小さくなったと考えられます.

一応, Mac 上での実行結果も載せておきます.

f:id:amedama41:20151125215051p:plain f:id:amedama41:20151125215104p:plain

Mac の mutex の実装はスピンロックを使用していないらしく, 同期のコストはかなり大きいです.

その他のテストケース

このほかにも producer/consumer や, ハンドラの中でハンドラを登録していくケースも試してみましたが, すべて似たような結果になりました.

結果として, io_service の同期コストは思ったよりも大きいことがわかりました.

まとめ

以下まとめです.

  • シングルスレッドで十分ならシングルスレッドモデルにする.

    • シングルスレッドで十分なパフォーマンスが得られるなら無駄に複雑にする必要はありません.
    • 時間のかかる処理はワーカースレッドで実行しましょう.
  • スレッドプールモデルよりも io_service-per-スレッドモデルを優先する.

    • 最大限スループットを出すならこのモデルです.
    • 色んなライブラリがスレッドプールを選択しているけど気にする必要はありません (nghttp2 は良く分かってそうな感じ).
  • スレッドプールモデルはハンドラの実行時間が短くない場合に検討する.

    • io_service の同期コストは小さくありません.
    • cpp-netlib の非同期サーバはスレッドプールも io_service 間のメッセージパッシングもしていますが, これは時間のかかる処理をすることを前提にした設計だからです.

io_service の同期コストはパフォーマンスにとって非常に影響が出ます.

実際, 実行時間が短いハンドラが主なサーバに cpp-netlib の真似をして Half-Sync/Half-Aync パターンの実装にしたら スレッド数 1 が最高スループットという残念な結果になったこともありました しかしその後 io_service-per-スレッドモデルに変更したらスループットが 2 倍以上向上しました *8.

io_service を使用する際は, 対象の特性をよく理解し用法, 用量を守って正しくお使いください.


この記事は C++ Advent Calendar 2015 14 日目の記事でした.

15 日目は hira_kuni_45 さんの記事です.

*1:非同期オペレーションのキャンセルを正しく行うのは思ったより単純ではないですが

*2:LinuxMac では 1 以外の値を指定する場合, 値の違いに特に意味はありません

*3:Recursive Mutex など必要ないと言ったがあれは嘘だ

*4:Recursive Mutex など必要ないと言ったがあれは嘘だと言ったがあれは嘘だ

*5:ただし, I/O Object が属さないスレッドからその I/O Object を操作する場合は, io_service::post 等を経由して操作する必要はあります

*6:多分この Strand は不要

*7:実装がいろいろおかしいので多分作者は Boost.Asio を正しく理解していないと思われます

*8:レイテンシに関してはモデルよりも io_service::poll 等を使用する方が影響は大きいです

initiating function 内で yield_context を呼べるようになった

Boost 1.58.0 以降では, Asynchronous operation の initiating function (async_write_some など) 内で直接 yield_context の handler の呼び出しが可能になりました.

以前まではどうだったかというと,

の順番で処理が実行される必要がありました.

例えば, io_service::dispatch は内部で handler を呼び出す可能性があるので上記の順番と逆の処理になります. そのため, この関数に yield_context を渡すのは今までは未定義動作でしたが, 1.58.0 からは問題なく動作します (そもそもこの関数に yield_context を渡すこと自体意味はないですが).

#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>

namespace asio = boost::asio;

int main(int argc, char const* argv[])
{
    asio::io_service io_service{};
    asio::spawn(io_service, [&](asio::yield_context yield) {
        io_service.dispatch(yield); // OK from Boost 1.58.0
    });
    io_service.run();
    return 0;
}

この変更によって, 今まで理論上コンテキストスイッチを必要としなかった部分を省略するといった最適化が可能になります (逆にいうと今までは必ずコンテキストスイッチが発生していました).

// ブロックなしで書き込みが可能な場合や,
// コネクションが Abort している場合等は
// コンテキストスイッチは必要無い.
socket.async_write_some(bufs, yield[ec]);

ただし, Asyncronous Operation の要求 では initiating function 内での handler の呼び出しを禁じているので, 今後このような最適化が行われるかはわかりません.

Boost Asio のドキュメントより一部抜粋.

When an asynchronous operation is complete, the handler for the operation will be invoked as if by:

Constructing a bound completion handler bch for the handler, as described below.
Calling ios.post(bch) to schedule the handler for deferred invocation, where ios is the associated io_service.

This implies that the handler must not be called directly from within the initiating function, even if the asynchronous operation completes immediately.

もしかしたら, asio_handler_is_continuation のような helper 関数を使用して実現するといったことも考えられます.

GCC 4.9.1 で List-initialization の評価順が直っていた

List-initialization におけるリストの各要素の評価順は左から右に評価されるように規定されています. しかし, GCC 4.9.0 以前では List-initialization でコンストラクタが呼び出される場合は正しい順序で引数が評価されていませんでした.

#include <iostream>

int f(int i)
{
    std::cout << i << std::endl;
    return i;
}

struct S {
    S(int, int, int) {}
};


int main()
{
    std::cout << "1 2 3 の順に出力されるはず" << std::endl;
    int a[3] = {f(0), f(1), f(2)};
    (void)a;

    std::cout << "出力順は規定されていない" << std::endl;
    S(f(0), f(1), f(2));

    std::cout << "1 2 3 の順に出力されるはず" << std::endl;
    S{f(0), f(1), f(2)};
}

GCC 4.9.0 での出力::

1 2 3 の順に出力されるはず
0
1
2
出力順は規定されていない
2
1
0
1 2 3 の順に出力されるはず
2
1
0

GCC 4.9.1 ではこのバグが修正されたので, 正しい順序で評価されます.

1 2 3 の順に出力されるはず
0
1
2
出力順は規定されていない
2
1
0
1 2 3 の順に出力されるはず
0
1
2

melpon.org

バグ報告されてから修正されるまで 2 年半以上もかかった息の長いバグでした.

51253 – [C++11][DR 1030] Evaluation order (sequenced-before relation) among initializer-clauses in braced-init-list

Boost 1.59 を GitHub リポジトリのソースからビルド

いつもは zip か, Homebrew でインストールしていたのを使用していましたが, 今回は GitHub から持ってきたのでメモを残しておきます.

# boost のトップリポジトリを取得
git clone https://github.com/boostorg/boost.git

# 各ライブラリのリポジトリを取得
cd boost
git submodule init
git submodule update

# Boost 1.59 をチェックアウト
git checkout -b boost-1.59.0 boost-1.59.0
git submodule update

# Clang C++11 を使用してビルドするように user-config.jam を編集
touch ~/user-config.jam
echo 'using clang : 3.6 : /usr/local/opt/llvm/bin/clang++ : <cxxflags>"-std=c++11 -stdlib=libc++" <linkflags>"-stdlib=libc++" ;' >> ~/user-config.jam
# Boost.MPI と Boost.Parallel Graph もビルドするように設定
echo "using mpi ;" >> ~/user-config.jam

# ヘッダファイル群の構成
b2 headers
# ICU_PATH を指定しないと regex のビルドに失敗するので ICU_PATH を指定してビルド
b2 --debug-configuration --layout=versioned -sICU_PATH=/usr/local/opt/icu4c stage 2>&1 | tee stage.log 

Boost.Build, OpenMPI, ICU は Homebrew でインストール済みでした.

Coroutine2 を使用するには C++14 でビルドする必要がありそうです.

Boost.Function を使わない SCOPE_EXIT_ALL

修正(2015-02-22) RVO を無効化した場合, 指定した処理が複数回実行されてしまうのを修正しました.

Boost.Scope Exit の実装を覗いてみたら, C++11 (or later) 用の BOOST_SCOPE_EXIT_ALL には Boost.Function が使用されていました. Boost.Function は動的メモリの確保を必要とすると思うので, 使わないでいい方法を考えてみました.

#include <iostream>
#include <utility>

template <class F>
struct guard
{
    explicit guard(F&& f) : f_(std::forward<F>(f)), callable_(true) {}
    guard(guard&& other)
        : f_(std::forward<F>(other.f_)), callable_(true)
    {
        other.callable_ = false;
    }
    ~guard() {
        if (callable_) f_();
    }
private:
    F f_;
    bool callable_;
};

namespace detail {
    struct guard_gen
    {
        template <class F>
        auto operator=(F&& f) const -> guard<F>
        {
            return guard<F>{std::forward<F>(f)};
        }
    };
} // namespace detail

// 変数名とかキャプチャの処理はここでは省略する
#define SCOPE_EXIT_ALL() \
    auto scope_exit = detail::guard_gen{} = []()

int main(int argc, char const* argv[])
{
    {
        SCOPE_EXIT_ALL() {
            std::cout << "exit" << std::endl;
        };
        std::cout << "start" << std::endl;
    }
    return 0;
}

実行結果

start
exit

Asio の coroutine で明示的に yield する

実行時間がかかる処理 A を coroutine の中で行うと, その coroutine と同じ strand 内の処理は処理 A が完了するまで待たされてしまいます.

io_service::post または strand::post を使用することで, 処理 A の合間で明示的に coroutine を切り替えることができます (dispatch は使用できません. 必ず post を使用してください).

#include <ctime>
#include <chrono>
#include <iostream>
#include <thread>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>

using namespace boost;

void heavy_task()
{
    std::this_thread::sleep_for(std::chrono::seconds{1});
}

auto print_now(char const* str)
    -> std::ostream&
{
    auto const now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
    return std::cout << str << " " << std::ctime(&now);
}

int main()
{
    asio::io_service io_service{};

    auto strand = asio::io_service::strand{io_service};

    // 実行時間のかかる処理 A
    asio::spawn(strand, [&](asio::yield_context yield) {
        print_now("start heavy_task");
        for (auto i = 0; i < 5; ++i) {
            io_service.post(yield); // ★ ここで切り替え
            heavy_task();
            io_service.post(yield); // ★ ここでも切り替え
        }
        print_now("finish heavy_task");
    });

    // 処理 A と同じ strand 内の処理
    asio::spawn(strand, [&](asio::yield_context yield) {
        asio::steady_timer timer{io_service};
        for (auto i = 0; i < 4; ++i) {
            timer.expires_from_now(std::chrono::seconds{1});
            timer.async_wait(yield);
            print_now("tick tack...    ");
        }
    });

    io_service.run();
}

明示的に切り替えた場合の実行結果

start heavy_task Tue Dec 23 11:21:13 2014
tick tack...     Tue Dec 23 11:21:14 2014
tick tack...     Tue Dec 23 11:21:15 2014
tick tack...     Tue Dec 23 11:21:16 2014
tick tack...     Tue Dec 23 11:21:17 2014
finish heavy_task Tue Dec 23 11:21:18 2014

切り替えない場合の実行結果

start heavy_task Tue Dec 23 11:32:17 2014
finish heavy_task Tue Dec 23 11:32:22 2014
tick tack...     Tue Dec 23 11:32:23 2014
tick tack...     Tue Dec 23 11:32:24 2014
tick tack...     Tue Dec 23 11:32:25 2014
tick tack...     Tue Dec 23 11:32:26 2014

ただし, スケジューリングのタイミングがシビアなので, 実行時間がかかるものは別スレッドで実行した方がいいでしょう.