読者です 読者をやめる 読者になる 読者になる

今年気になった C++ ライブラリとかフレームワークを紹介する記事

C++

この記事は C++ Advent Calendar 2014 の17日目の記事です.前日は @yutopp さんの Boost.Spirit.X3のご紹介 - C++ Advent Calendar 2014(16日目) でした.

最近 Boost.Spirit と LLVM で言語をちまちまと書いているので,Boost.Spirit と LLVM で言語つくるチュートリアルみたいなのを書こうと思ったのですが,チュートリアル用の言語の設計をあれこれ考えているうちに時間が経ってしまいました…ぼちぼち考えているので,別の機会に書きます.

というわけで,今回は年の暮れということもあり,今年 GitHub の C++ Trending repositories で見た,気になるライブラリやフレームワーク7つについて紹介しようと思います.

紹介するライブラリやフレームワーク

cppformat

cppformat は文字列を整形(フォーマッティング)するためのライブラリです. この手の処理は C だと sprintf()C++ だと stringstream,Boost ライブラリだと Boost.Format がありますが,cppformat は Boost.Format のような強力なフォーマットをサポートしつつ処理が高速というのが売りのようです.

{} による Pythonstr.format ライクなフォーマットや,printf() フォーマット,引数の番号指定が使えます.

// Hello, world!
std::string s1 = fmt::format("{}, {}!", "Hello", "world");

// Goodbye, world!
std::string s2 = fmt::sprintf("%s, %s!", "Goodbye", "world");

// Hello, world and goodbye world!
std::string s3 = fmt::format("{1}, {0} and {2}, {0}!", "world", "Hello", "goodbye")

変換には iostream のように operator<< が使われるため,operator<< が正しく定義されていればユーザ定義型でも使えます.

struct date {
    int y, m, d;

    date(int y, int m, int d)
        : y(y), m(m), d(d)
    {}

    std::ostream &operator<<(std::ostream &o, date const&rhs)
    {
        return o << rhs.y << '/' << rhs.m << '/' << rhs.d;
    }
};

// Today is 2014/12/17
std::string s = fmt::format("Today is {}", date{2014, 12, 17});

フォーマットのシンタックスについてもう少し詳しく書くと,

{[引数番号][:フォーマット指定]}

となっており,フォーマット指定 のところには文字列のアライン(左詰め,右詰め,中央寄せ)や数値のフォーマット指定が行えます.

// 左詰め: "left aligned                  "
fmt::format("{:<30}", "left aligned");

// 文字指定で中央詰め: "***********centered***********"
fmt::format("{:*^30}", "centered");

// 整数値のフォーマット指定: "int: 42;  hex: 2a;  oct: 52; bin: 101010"
fmt::format("int: {0:d};  hex: {0:x};  oct: {0:o}; bin: {0:b}", 42);

// 浮動小数点数のフォーマット指定例(符号を常に表示): "+3.140000; -3.140000"
fmt::format("{:+f}; {:+f}", 3.14, -3.14);

フォーマットの構文の詳細は こちら にあります.

cppformat にはこの他に Writer API というのもあり,stringstream のように,フォーマット指定で文字列を文字列ストリームに書き込んでいき,最後に str() メンバ関数std::string として取り出すこともできます.

最初に書いたように, cppformat はそのパフォーマンスも売りのようで,ドキュメントにはいくつかベンチマークが載っています.

それによると,

  • 実行時のパフォーマンスでは std::ostream を凌いで printf の次点
  • バイナリサイズは iostream の半分程度で printf とほぼ同等
  • コンパイル時間はそれなりにかかるものの,それでも iostream の2倍程度

らしいです.

cppformat に近い高性能なフォーマットを提供する Boost.Format にはパフォーマンスの面で cppformat のほうが遥かに有利(実行速度8倍ぐらい,コンパイル時間5倍ぐらい,バイナリサイズ10倍ぐらい)と主張しています. 別の数値から文字列への変換のベンチマーク によると,Boost.Spirit.Karma よりも高速らしいです.

json11

json11 は Dropbox が開発した JSON ライブラリです.ライブラリ名からも分かるように,C++11 を有効にしてビルドする必要があります.

一番の特徴としては,initializer_list を用いて JSON オブジェクトの構築ができます.

Json user = Json::object {
    { "name", "Linda_pp" },
    { "private", false },
    { "interests", Json::array { "C++", "Vim", "Inu" } },
};

// 文字列として JSON を出力
std::string json_str = user.dump();

// operator[] で簡単にアクセスできる
std::string first_interest = user["interests"][0].string_value();

また,to_json() メンバ関数を実装することで,ユーザ定義型も簡単に JSON にアダプトできます.

class Point {
public:
    int x;
    int y;
    Point (int x, int y) : x(x), y(y) {}
    Json to_json() const { return Json::array { x, y }; }
};

std::vector<Point> points = { { 1, 2 }, { 10, 20 }, { 100, 200 } };
std::string points_json = Json(points).dump();

残念ながら公式のドキュメントはまったく無いのですが,json11.cpp と json11.hpp 併せても 1000 行いかないのでざっと全部コードを読むのが一番良さそうです.

内部的には各 JSON オブジェクトを std::shared_ptr で管理しており,各種 STL コンテナを使うので rapidjson ほどパフォーマンスは良くなさそうですが,initializer list での JSON の構築がとても楽なのでそういう用途には使えそうです.

cppitertools

cppitertools は Python の itertools に似たインターフェースをもつ C++11 向けのシーケンス処理ライブラリです. シーケンス処理の定番?な Fizz Buzz を書くとこんな感じになります.

for (auto const& fb : iter::imap([](auto const i) -> std::string {
                if (i%15 == 0) {
                    return "fizzbuzz";
                } else if (i%3 == 0) {
                    return "fizz";
                } else if (i%5 == 0) {
                    return "buzz";
                } else {
                    return std::to_string(i);
                }
            }), iter::range(1, 100)) {
    std::cout << fb << std::endl;
}

ここでは任意個数のシーケンスを受け取って,それぞれの要素にラムダ式を適用できる imapHaskell でいう zipWith みたいなの)を使いましたが,これ以外にもたくさんのシーケンス処理用関数があります.

  • range()
  • enumerate()
  • zip()
  • imap()
  • filter()
  • filterfalse()
  • unique_everseen()
  • unique_justseen()
  • takewhile()
  • dropwhile()
  • cycle()
  • groupby()
  • accumulate()
  • compress()
  • chain()
  • chain.from_iterable()
  • reversed()
  • slice()
  • sliding_window()
  • grouper()

だいたい名前で想像がつくと思いますが,それぞれの関数の詳細については cppitertools の README を読んでみてください. また,ここでは iter::range() を使いましたが,std::vector などのコンテナも使えます.

それぞれのシーケンス処理関数はシーケンス適用後を表すオブジェクトを返すだけで,呼び出し時点ではまだ処理を行いません.つまり,実際の処理は for 文適用時まで遅延されます. よって,iter::filter() で偶数だけ取り出して iter::imap() で文字列化して… といった複数のシーケンス処理関数をつなげても中間のシーケンスはつくられません.(RubyEnumerable::Lazy みたいな感じ) また,各シーケンス処理関数の戻り値型はすべて range-based-for 文でイテレートするための要件を満たしているので for 文で受けることができます.

個人的には Boost.Range と range adaptor が好きですが,cppitertools はシーケンス処理関数の種類が豊富なのが良いなぁと思います.

crow

crow は C++11 用でヘッダオンリーなマイクロウェブフレームワークです.Python の Flask を参考にしたインターフェースになっていて,Flask のように簡潔に記述でき,かつ C++ の高いパフォーマンスを活かせるのが特徴です.

int main()
{
    crow::SimpleApp app;

    CROW_ROUTE(app, "/about")
    ([](){
        return "About Crow example.";
    });

    // simple json response
    CROW_ROUTE(app, "/json")
    ([]{
        crow::json::wvalue x;
        x["message"] = "Hello, World!";
        return x;
    });

    app.port(18080)
        .multithreaded()
        .run();
}

ルートを定義しつつ,各ルートごとの処理がその場に書けます.

さらにおもしろいのは,ルートの中にタグを書くことができ,そのタグに対応した型の引数を持つラムダ式を受け取れる点です.

CROW_ROUTE(app,"/hello/<int>")
([](int count){
    if (count > 100)
        return crow::response(400);
    std::ostringstream os;
    os << count << " bottles of beer!";
    return crow::response(os.str());
});

ここではルートの中に <int> タグが一つあり,そのタグの位置に指定された値がラムダ式の引数 count に渡ってきます. このタグとラムダ式の引数の解析はコンパイル時に行われます.よって,下記のようなタグとラムダ式の引数の食い違いはコンパイルエラーになります.

CROW_ROUTE(app,"/oops/<int>")
([](int a, int b){
    return crow::response(500);
});

このあたりの実装は以前ブログにまとめたので,気になる方はそちらを読んでください.

C++ の web フレームワーク crow のコンパイル時処理

また,crow には json を処理するためのパーサ(crow::json)や Mustache ベースのテンプレートライブラリ(crow::mustache)もあり,簡単なウェブアプリをつくるのに十分なものがそろっているようです. さらに OR Mapper も開発中らしいです.あと,気になる人は気になるベンチマークこちら にあります.

Catch

Catch は C++ 向けの単体テストフレームワークです.

#define CATCH_CONFIG_MAIN
#include "catch.hpp"

unsigned int Factorial( unsigned int number ) {
    return number <= 1 ? number : Factorial(number-1)*number;
}

TEST_CASE( "Factorials are computed", "[factorial]" ) {
    REQUIRE( Factorial(1) == 1 );
    REQUIRE( Factorial(2) == 2 );
    REQUIRE( Factorial(3) == 6 );
    REQUIRE( Factorial(10) == 3628801 ); // Failed!
}

ヘッダオンリーで簡潔に各テストケースを書くことができます. アサーションのレベルは2つあり,REQUIRE は失敗するとそこで実行を中止,CHECK は失敗してもテストを継続します. また,テストケース内に SECTION() マクロで複数のセクションをつくることもできます.SECTION() マクロによるブロックは自由にネストできます.

失敗するとテスト実行時に下記のようなメッセージが表示されます.

main.cpp(12): FAILED:
  REQUIRE( Factorial(10) == 3628801 )
with expansion:
  3628800 == 3628801

Factorial(10) が展開されて,どういう値で失敗したのかが一目でわかるようになっています.

アサーションマクロは条件式を記述できる REQUIRE(expr) のほかに例外をテストできる REQUIRE_THROWS(expr)/REQUIRE_NOTHROW(expr)REQUIRE_THROWS_AS(expr, expr_type) などがあります.さらに matcher を指定して,与えられた式が matcher を満たすかどうかをチェックする REQUIRE_THAT(expr, matcher) もあります.matcher は include/internal/catch_matchers.hpp を見るといくつかデフォルトのものがあり,Matcher<ExpressionT> を継承することで自作もできるようですが,まだ開発中らしくドキュメントは見当たりませんでした.

さらに,BDD 向けのマクロも提供されています.

SCENARIO( "vectors can be sized and resized", "[vector]" ) {

    GIVEN( "A vector with some items" ) {
        std::vector<int> v( 5 );

        REQUIRE( v.size() == 5 );
        REQUIRE( v.capacity() >= 5 );

        WHEN( "the size is increased" ) {
            v.resize( 10 );

            THEN( "the size and capacity change" ) {
                REQUIRE( v.size() == 10 );
                REQUIRE( v.capacity() >= 10 );
            }
        }
    }
}

BDD 用のマクロは前述の TEST_CASE マクロなどと対応しています.

普通のスタイル BDD スタイル
TEST_CASE SCENARIO
SECTION GIVEN, WHEN, THEN
ない AND_WHEN, AND_THEN

AND_WHENAND_THENWHENTHEN のあとにつなげて書きやすいようにするためのマクロです.

より詳細にはこちらの記事が参考になります.

C++ Testing Framework の Catch を使ってみた

Google Test や Boost.Test は高機能ですが,大きすぎてドキュメントを読むのも大変だったりするので,さっとテストを書ける Catch のお手軽感は良いなぁと思いました.

cpplinq

cpplinq は .NetLinQ を参考に作られた,範囲に対する処理を書ける高階関数群ライブラリです.先に紹介した cppitertools や Boost.Range と用途が似ています. 開発は GitHub で行われていませんが,上記の cppitertools 関連で気になったので紹介します. C++LinQ を移植する系ライブラリは LinQ++ とか他にもあるらしいですが,それに比べて operator>>オーバーロードで新たに追加できる点や C++11 以降を採用していてプリプロセッサに頼っていない点,パフォーマンスなどが売りらしいです.

サンプルコードはこんな感じです.

#include "cpplinq.hpp"

int computes_a_sum ()
{
    using namespace cpplinq;
    int ints[] = {3,1,4,1,5,9,2,6,5,4};

    // Computes the sum of all even numbers in the sequence above
    return
            from_array (ints)
        >>  where ([](int i) {return i%2 ==0;})     // Keep only even numbers
        >>  sum ()                                  // Sum remaining numbers
        ;
}

ここでは最初のシーケンスを作成するために from_array() が使われていますが,STL コンテナからシーケンスを作成する from()イテレータから作成する from_iterators(),直接シーケンスを作成する range() もあります.

シーケンスに対する処理には,要素をフィルタする where() の他にも,要素を map する select() やシーケンスをソートする ordereby_ascending() など様々な関数(Query Operator と呼ばれているらしい)があります. Query Operator によってシーケンスをフィルターしたりマップしたり区切ったりつなげたり集合として扱ったり変換したりできます.

各 Query Operator の述語にはラムダ式が使われており,C++14 ではラムダ式の引数の型に auto が使えるので,ますます書きやすくなりそうです.

Boost.Spirit.X3

演算子をオーバーライドしまくることによって言語内 DSL によってパーサを書ける変態的なパーサジェネレータフレームワーク Boost.Spirit の後継です. X3 の紹介は前日の記事で紹介されているので詳しくは触れませんが,いくらか定数式によって再実装されたりしてコンパイルが高速になったり,エラーメッセージもかなり分かりやすくなっているようです.また,セマンティックアクションにラムダ式を使えます.

僕も今書いているコンパイラのパーサを V2 から X3 に乗り換えることを検討しましたが,パーサの種類が少ないことやドキュメントが無いこと,テストがしょぼいことから様子見することにしました.

まとめ

今年 GitHub で話題になった C++ ライブラリについて紹介しました.他にも GPU 演算向け汎用ライブラリ ArrayFire とか気になったのですが,GPU 関連全然分からないので見送りました.

紹介したライブラリの中にも C++11 以上限定のライブラリがそれなりにあったり,GCCC++11 をデフォルトにする実装を入れて Clang がそれに追従したりといったこともあり,C++11 も広まってきているという印象があります.今後も C++11 や C++14 といった最新の規格の機能を利用したライブラリが出てくるのが楽しみなので,引き続きウォッチしていきたいと思っています.

明日は @ignis_fetuus さんです.