[C++]自称世界最速の文字列数式計算ライブラリを作った。

更新情報

2021年1月

  • C++標準への要求をC++14からC++17に変更しました。
  • ベクトル、行列をadapt::Vector<double>、adapt::Matrix<double>からEigenのVectorXd、MatrixXdに、文字列をadapt::Stringからstd::stringに変更しました。
  • 上の変更に伴い、線形代数ライブラリEigenを要求するようになりました。

動機

C++で文字列式を計算したいと考えたことはないだろうか。例えば"2 * a + b ^ 2"みたいな適当な式を文字列で定義しておいて、aやbに任意の値を与えることで計算結果を出力させたい、というような話だ。pythonでいうところのevalみたいなもの。
これを実現するようなライブラリは、実は多数存在する。現存する中で最速のものはExprTkのようで、似たライブラリを集めたベンチマークで最高成績を収めている。

ただしExprTkには欠点がある。基本的に浮動小数点しか扱えないのだ。いや正確には複素数なども扱えるが、基本的に予めテンプレートパラメータに与えた一つの型しか扱えない。浮動小数点と決めたなら整数も複素数も扱えないのだ。もちろんベクトルや行列、文字列などを扱うことはできないし、増して同時に扱うことも不可能である。多数の型に対応しつつも高速なライブラリは、残念ながら見つからなかった。私はこれらの型をすべて同時に、自由に扱いたいのだ。

そこで、これらの型を一つの式の中で同時に扱うことのできる文字列式パーサーライブラリを自作しようと考えた。そしてどうせなら、ExprTkを上回る世界最速のライブラリにしてやろうと思った。

というわけで試作してみたのがADAPT-FMPである。要求するバージョンはC++14で、ヘッダーオンリーで使える2021年1月現在のバージョンでは線形代数ライブラリEigenとC++17が必要であるが、一応ヘッダーオンリーではある。開発したのは2020年1月ごろで、その後自作のデータ解析用ライブラリに組み込もうとしたのだが、そちらの作業を行う時間がなかなか取れずに放置されていた。折角作ったのに勿体ないのでここで公開してみる。 github.com

ADAPT-FMPの使い方

現在対応している型は整数(int64_t)、浮動小数点(double)、ベクトル(adapt::Vector<double>Eigen::VectorXd)、行列(adapt::Matrix<double>Eigen::MatrixXd)、文字列(adapt::Stringstd::string)である。とはいえ、ベクトルや行列、文字列の関数や演算子などは十分には定義されていない。そのうち気が向いたら用意する。 定義済み演算子、関数はGitHubのREADMEFMPFunction.hあたりを参照されたい。

#include <ADAPT/FMP/FastMathParser.h>

using namespace adapt;
using namespace adapt::fmp;
using namespace adapt::fmp::lit;

int main()
{
    {
        std::string expr = "25*x^5 - 35*x^4 - 15*x^3 + 40*x^2 - 15*x + 1";
        double x;
        double r0 = 0;
        double r1 = 1;
        double delta = 1 / 100.0;
        FastMathParser f(expr, { "x"_a = &x });
        for (x = r0; x <= r1; x += delta)
        {
            printf("%19.15f\t%19.15f\n", x, f.Flt());
        }
    }
    {
        std::string expr1 = "sqrt(dot(xy, xy))";
        std::string expr2 = "atan2(xy[1], xy[0])";
        Eigen::VectorXd xy(2); xy << 1., 1.;
        FastMathParser r(expr1, { "xy"_a = &xy });
        FastMathParser t(expr2, { "xy"_a = &xy });
        for (double theta = 0; theta < 1.; theta += 0.01)
        {
            xy[0] = cos(theta);
            xy[1] = sin(theta);
            printf("r = %lf, t = %lf\n", r.Flt(), t.Flt());
        }
    }
    {
        std::string expr = "mat2(vec2(cos(pi * t), -sin(pi * t)), vec2(sin(pi * t), cos(pi * t)))*v + vec2(0.1, 0.5)";
        double theta;
        Eigen::VectorXd v(2);
        FastMathParser aff(expr, { "t"_a = &theta, "v"_a = &v }, { "pi"_c = 3.1415926535897932});
        for (double x = 0; x < 1; x += 0.1)
        {
            for (double y = 0; y < 1; y += 0.1)
            {
                for (theta = 0; theta < 1; theta += 0.05)
                {
                    v[0] = x, v[1] = y;
                    const Eigen::VectorXd& res = aff.Vec();
                    printf("(%8.5lf, %8.5lf) -> (%8.5lf, %8.5lf)\n", x, y, res[0], res[1]);
                }
            }
        }
    }
    return 0;
}

使い方は上記の通り。FastMathParserのコンストラクタに式と引数を与え、計算用関数を呼び出せば良い。ただし式の戻り値の型によって呼び出すべき関数が異なる。

  • int64_t -> Int()
  • double -> Flt()
  • std::string -> Str()
  • Eigen::VectorXd -> Vec()
  • Eigen::MatrixXd -> Mat()

戻り値の型が分からない場合、FastMathParser::GetResultType()を呼べば教えてくれる。

余談

このライブラリの開発は実はかなり難航した。最初私はテンプレートを駆使してこれの構造を纏め上げようとしたのだが、それはうまく行かなかった。完全にテンプレートメタプログラミングのみで実装したところ、複雑化しすぎてコンパイルができなかったのだ。あの巨大すぎるテンプレートライブラリをコンパイルできるほど現代のPCやコンパイラの性能は高くないようだ。デバッグ情報を捨てれば一時間くらいかけてコンパイルできることは分かったが、ファイルサイズが悲惨なことになったため断念した。
仕方ないので、Pythonを使ったコード生成を行うことにした。テンプレートに担わせようとしたジェネリクスを代わりにPythonスクリプトに全部書き下させることで、テンプレートを排除しつつ高速化することができた。……いや、まあ、すごい力技なんだけど、これ以外に方法がなかったのだ。ExprTkはdoubleなどの1種類の型に対してだけ実装すればいいが、こちらは現時点で5種類もの型を同時に扱うので、手作業で書き下すなんて到底不可能なのである。

適当に数式を入力してExprTkと速度比較をしてみると、状況によりけりではあるが、ADAPT-FMPの方が数%程度だが速いことが多かったので、世界最速を自称している。詳細なベンチマークなどは行っていないので確証はないし、ExprTkの方が高速な場合もあるちょっとしたベンチマークもやってみたところ、実行速度では概ねExprTkを上回っているらしいことが分かった。もちろん式によってどちらに有利かという差異はあるので一概に言えないが、世界最速を名乗っても不自然ではないと思っている。 また実行速度と多数の型の扱いを両立する代償としてコンパイルは遅い。

ちなみに、パーサージェネレータにLemonを、レキサージェネレータにre2cを使っている。使いやすいのだが、どちらも基本的にC言語用であることが不便だった。C++でこれを使うにはちょっと工夫が必要だった。特にLemon。何でもかんでも共用体にぶっ込まれるととても困る。しかもLemonによって生成されたコードから消しようのない警告が多数出てくる。もっと便利なジェネレータがあればいいのだが、私が探した限りでは見つからなかった。

最近GitHubで公開していたとあるリポジトリに初めて星が付いて気が大きくなり、以前から公開するか悩んでいたこちらも踏み切った。他にSlack過去ログビューアもいずれ公開しようと思っているが、まだメッセージ検索機能が実装できていないこと、Qtに依存しているためオープンソース化にちょっと手間取っていることが原因で保留している。

CMake-guiが起動しなくなったときの対処。

Windows10でCMake-guiを使っているのだが、ある日突然どういうわけか起動しなくなった。タスクバーにはCMake 3.16.4というボタンがアイコン付きで表示されるが、ウィンドウが現れないのだ。CMakeの再インストールを行っても直らない。これは3.18.0にアップデートしても同様だった。

この対処が正しいのかどうかは私もよく分かっていないので、安全性は保証できないがレジストリ中のComputer\HKEY_CURRENT_USER\Software\Kitware\CMakeSetupというキーを削除すると正常に起動するようになった。この中には前回のCMake使用時の設定などが保存されているらしく、そのゴミ掃除をしたわけである。

[C++]enumを文字列に変換する汎用的な方法。

2021年9月28日追記。どういうわけかアクセス数が増えているので、訪問者を混乱させないよういい加減だったサンプルを作り直した。

enumは単なる整数値に名前を付与する手段の一つだ。パラメータの意味がわかりやすくなる上、処理コストの大きな文字列を使わなくても良い便利な機能である。最近ではenum classの形で使われることが推奨されている。

enum DoW { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, };
DoW dow = Sunday;
int i = (int)dow;//i == 0

ただ例えば標準出力などにその名前の方を出力したいと思っても、ちょっと手間がかかる。enumは本質的にはただの整数値なので、例えばprintfの%sに直接enumを渡すことは出来ない。ネット上に溢れかえっている方法は残念ながらコードが簡便であるがenumのメンバを修正したりするたびにメンテナンスコストが発生するので厄介だ。

が、実は非常に美しく実現する方法がある。もし自力で実装するのが面倒なら、magic_enumというライブラリを使えばMSVC、Clang、GCCXcodeいずれでも環境の差が吸収され簡単にできるようになる。長々とした説明を読みたくない人は下のmagic_enumという項までスクロールすべし。

愚直な方法

もし文字列に変換したいのなら、通常はswitch文などを使って値ごとに処理を分岐しなければならない。と一般には思われている。

enum DoW { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, };
DoW e = Tuesday;
//printf("%s\n", e); これは不可能。
switch (e)
{
case Sunday: printf("Sunday\n"); break;
case Monday: printf("Monday\n"); break;
case Tuesday: printf("Tuesday\n"); break;
...
}

これはCやC++の初心者でも理解できる単純な方法という意味では優れている。しかし記述コストが大きくなるし、列挙型に新たなメンバを追加した際は修正しなければならない。

std::unordered_mapboost::bimapsを使って対応付けることで変換することも出来るが、メンテナンスコストは大きいままだ。

あるいはマクロを使う手もしばしば提示されるが、これは良くない。マクロはコンパイルの直前に展開されるため、変数の中身は文字列化できないのだ。

#define VarName(name) #name
printf("%s\n", VarName(Tuesday));//これは正しく"Tuesday"と出力されるが、
DoW e = Tuesday;
printf("%s\n", VarName(e));//これでは"Tuesday"ではなく"e"と出力される。

だが実は、他にも方法がある

少しスマートな方法

自前で分岐させる他にも、enumの変数名を取得する方法は存在しないわけではないのである。鍵となるのは__FUNCSIG__(Clang、GCC等では__PRETTY_FUNCTION__)という定義済みマクロだ。これは関数の情報を返してくれるもので、例えば次のようにfuncという関数内でこのマクロの中身を見てみると、

int func(int x)
{
    //MSVCの場合
    std::cout << __FUNCSIG__ << std::endl;//int __cdecl func(int)
    //GCC、Clangの場合
    std::cout << __PRETTY_FUNCTION__ << std::endl;//int func(int)
    return x;
}

このように、マクロの外側の関数についての情報を教えてくれる。これを利用する。次の関数を考えてみよう。

enum DoW { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, };

template <class Enum, Enum E>
void func()
{
    //Enum = Dow, E = Sundayが与えられているとする。
    //MSVCの場合
    std::cout << __FUNCSIG__ << std::endl;//void __cdecl func<enum DoW,Sunday>(void)
    //GCC、Clangの場合
    std::cout << __PRETTY_FUNCTION__ << std::endl;//void func() [Enum = DoW, E = Sunday]  GCCとClangで微妙に異なる。
}

面白いことに、テンプレートパラメータにenumのメンバを渡すと、__FUNCSIG__の関数名にそのメンバの文字列が含まれるようになるのだ。これはtypeidなどには出来ない芸当である。あとはこの情報から前半と後半に付随する余計な文字を削除すれば、enumから文字列への変換が可能になる。え、これじゃ実行時変数からは文字列に変換できないじゃんって?そこは配列の出番である。予め"enumの各メンバに対応する文字列を格納した配列"を生成しておき、実行時にそこから適宜取り出せばよい。

この仕組をとてもいい加減に作ってみると以下のような感じになる。 とりあえずMSVC2019、Clang13.0.0、GCC12.0.0で動作を確認した。C++14以上で動くはずである。

#include <iostream>
#include <string>
#include <utility>
#include <array>

template <size_t N>
struct StaticString
{
    StaticString(const char* c)
        : StaticString(c, std::make_index_sequence<N>())
    {}

    std::string str() const { return std::string(mStr); }

private:
    template <size_t ...Indices>
    StaticString(const char* c, std::index_sequence<Indices...>)
        : mStr{ c[Indices]..., '\0' }
    {}

    char mStr[N + 1];
};

constexpr size_t FindChar(const char* str, char f)
{
    size_t res = 0;
    while (*str != f && *str != '\0') ++res, ++str;
    return res;
}

template <class M, M m>
constexpr auto EnumName()
{
#ifdef _MSC_VER
    constexpr char name[] = { __FUNCSIG__ };
    constexpr size_t s = FindChar(name, ',') + 1;
    constexpr size_t e = FindChar(name, '>');
#else
    constexpr const char* name_ = __PRETTY_FUNCTION__;
    constexpr const char* name = name_ + FindChar(name_, '=') + 1;
    constexpr size_t s = FindChar(name, '=') + 2;
    constexpr size_t e = FindChar(name, ']');
#endif
    constexpr size_t N = e - s;
    return StaticString<N>(name + s);
}
template <class Enum, class Indices>
struct EnumNames;
template <class Enum, std::underlying_type_t<Enum> ...Indices>
struct EnumNames<Enum, std::integer_sequence<std::underlying_type_t<Enum>, Indices...>>
{
    static const std::array<std::string, sizeof...(Indices)> msNames;
};
template <class Enum, std::underlying_type_t<Enum> ...Indices>
const std::array<std::string, sizeof...(Indices)>
EnumNames<Enum, std::integer_sequence<std::underlying_type_t<Enum>, Indices...>>::msNames
= { EnumName<Enum, (Enum)(Indices)>().str()... };

template <class Enum, std::underlying_type_t<Enum> Max = 128>
std::string GetEnumName(Enum e)
{
    using UType = std::underlying_type_t<Enum>;
    static const auto& names = EnumNames<Enum, std::make_integer_sequence<UType, Max>>::msNames;
    return names[(UType)e];
}

enum DoW { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, };
enum class Month { January, February, March, April, May, June, July, August, September, October, November, December, };

int main()
{
    std::cout << GetEnumName(Monday) << std::endl;//Monday
    std::cout << GetEnumName(Saturday) << std::endl;//Saturday
    std::cout << GetEnumName(Month::August) << std::endl;//Month::August
    return 0;
}

特に複雑なことはしていない。単に__FUNCSIG__または__PRETTY_FUNCTION__によって得られる文字列からenumのメンバ名の部分を切り取り、配列に収め、GetEnumNameで配列から該当する名前を取得しているだけである。

この方法の最大の利点は、あらゆるenumに対して使えることだ。新たなenumを定義したりメンバを追加したりしても上のコードを書き直す必要がない。

しかし問題もある。上の実装はenumの各値が0から127までの整数であることを前提としており、その範囲外の値を取る場合は対応できない。しかしC++に列挙型のメンバの一覧を取得する方法が用意されているわけでもないので、これを汎用的に実装するのはいろいろな小技が必要になり、非常に手間だ。

magic_enum

そこで登場するのがmagic_enumである。このあたりのややこしいメタプログラミングを全部実装してくれていて、非常に簡単にenumと文字列の相互変換を可能にしてくれる最高のライブラリだ。相互変換である。上ではenumから文字列への変換しか実装しなかったが、こちらは文字列からenumへの変換まで可能にしてくれる万能っぷり。その実装の美しさはもうコードを読み進めるごとに感動で涙してしまうほどである。シングルヘッダーなのでファイル一つコピペしてincludeするだけで使えるところも素晴らしい。

#include <iostream>
#include <magic_enum.hpp>

enum DoW { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, };

int main()
{
    std::cout << magic_enum::enum_name(Sunday) << std::endl;//Sunday
    std::cout << magic_enum::enum_cast<DoW>("Wednesday").value() << std::endl;//3
    return 0;
}

こんなにも簡単に相互変換できてしまう。絶賛せずにいられるか。
C++17以上が必要、かつ依存ライブラリが増えてしまうが、それが問題にならないのなら導入するとよいだろう。それが不可能なら上のサンプルを頑張って改造しよう。