[C++][ADAPT]C++用Gnuplotライブラリを作った。

作った(嘘)1

もう更新することはないと思っていたC++GnuplotライブラリADAPT-GPM2であるが、データ分析ライブラリADAPTへ取り込むことでメンテナンスを再開した。

配布先のリポジトリはこちらへ

C++には既にMatplot++というライブラリがあり、私もこの一年ほどはあちらを導入しようと色々な調整を行っていたのだが、残念ながら様々な問題に直面したので断念し、昔の自作ライブラリに戻ってきたのである。
元々あったGitHubリポジトリアーカイブし、今後はデータ分析ライブラリADAPTの一機能としてメンテナンスを続ける。ADAPTの一部として配布することになるので、クローンすると余計なコードがくっついてきてしまうが、#include <OpenADAPT/Plot/Canvas.h>のようにプロット機能だけをincludeすることは可能だ。

使い方

以前のものと使い方はほぼ同じだが、統合に伴い名前空間やクラス名、内部実装などを整理している。若干コードの書き方が変わったので、以前のサンプルコードを現バージョン用に書き直したものを残しておく。とはいえ表面的にはそれほど大きな変化はなく、GPM2という名称が削除されたことくらいだ。名前空間adapt::gpm2がなくなってadapt単独になり、GPMCanvas2Dなどのクラス名からGPMが消えてCanvas2Dのように短縮された程度。
あとは細かいところで言えば、plot::color = "light-red"plot::pointtype = 7plot::style = Style::stepsのようなオプション指定の長たらしさ分かりにくさは気に食わない部分の一つだったので、plot::c_light_redplot::pt_fcirplot::s_stepsなどのように短縮版を用意した。従来通りの書き方でも記述が少々長い以外に問題はない。

ここに乗せていないサンプルについてはExamplesを参照。

例.1 散布図、可変サイズ

int main()
{
    std::vector<double> longitudes{ 141.3469, 140.74, 141.1526, 140.8694, 140.1023, 140.3633, 140.4676, 140.4468, 139.8836, 139.0608, 139.6489, 140.1233, 139.6917, 139.6423, 139.0235, 137.2113, 136.6256, 136.2219, 138.5684, 138.1812, 136.7223, 138.3828, 136.9066, 136.5086, 135.8686, 135.7556, 135.5023, 135.183, 135.8048, 135.1675, 134.2383, 133.0505, 133.9344, 132.4553, 131.4714, 134.5594, 134.0434, 132.7657, 133.5311, 130.4017, 130.3009, 129.8737, 130.7417, 131.6126, 131.4202, 130.5581, 127.6809 };
    std::vector<double> latitudes{ 43.0642, 40.8244, 39.7036, 38.2682, 39.7186, 38.2404, 37.7503, 36.3418, 36.5658, 36.3911, 35.8569, 35.6051, 35.6895, 35.4475, 37.9026, 36.6953, 36.5944, 36.0652, 35.6642, 36.6513, 35.3912, 34.9756, 35.1802, 34.7303, 35.0045, 35.021, 34.6937, 34.6913, 34.6851, 34.226, 35.5036, 35.4723, 34.6618, 34.3853, 34.1858, 34.0658, 34.3402, 33.8416, 33.5597, 33.5902, 33.2635, 32.7448, 32.7898, 33.2382, 31.9077, 31.5602, 26.2124 };
    std::vector<double> populations{ 5250, 1230, 1220, 2330, 970, 1070, 1840, 2860, 1940, 1930, 7330, 6290, 13960, 9200, 2200, 1040, 1140, 770, 810, 2030, 1970, 3630, 7550, 1790, 1410, 2580, 8820, 5450, 1310, 930, 550, 670, 1890, 2810, 1320, 720, 960, 1340, 690, 5100, 810, 1280, 1720, 1130, 1080, 1590, 1450 };
    auto pop_size = populations | std::views::transform([](double x) { return x / 1000; });

    namespace plot = adapt::plot;
    adapt::Canvas2D g("example_scatter.png");
    g.SetXRange(128, 150);
    g.SetYRange(29, 46);
    g.SetSizeRatio(1);
    g.SetXLabel("longitude");
    g.SetYLabel("latitude");
    g.SetTitle("Japan population distribution");
    // world_10m.txt can be downloaded from https://gnuplotting.org/plotting-the-world-revisited/
    g.PlotPoints("PlotExamples/world_10m.txt", "1", "2", plot::notitle, plot::c_dark_gray, plot::s_lines).
        PlotPoints(longitudes, latitudes, plot::notitle,
                   plot::s_points, plot::pt_fcir, plot::color_rgb = "0xAA6688FF", plot::variable_size = pop_size);

    return 0;
}

例.2 関数、エラーバー

#include <random>
#include <OpenADAPT/Plot/Canvas.h>

int main()
{
    std::string norm = std::to_string(250. / std::sqrt(2 * 3.1415926535));
    std::string equation = norm + "*exp(-x*x/2)";

    std::mt19937_64 mt(0);
    std::normal_distribution<> nd(0., 1.);
    std::vector<double> x1(32, 0);
    std::vector<double> y1(32, 0);
    std::vector<double> e1(32);
    for (int i = 0; i < 1000; ++i)
    {
        double x = nd(mt);
        if (x < -4.0 || x >= 4.0) continue;
        ++y1[static_cast<size_t>(std::floor(x / 0.25) + 16)];
    }
    for (int i = 0; i < 32; ++i)
    {
        x1[i] = i * 0.25 - 4. + 0.125;
        e1[i] = std::sqrt(y1[i]);
    }

    namespace plot = adapt::plot;
    adapt::Canvas2D g("example_2d.png");
    //g.ShowCommands(true);
    g.SetTitle("example\\_2d");
    g.SetXRange(-4.0, 4.0);
    g.SetXLabel("x");
    g.SetYLabel("y");
    g.PlotPoints(equation, plot::title = "mu = 0, sigma = 1",
                 plot::s_lines).
        PlotPoints(x1, y1, plot::xerrorbar = 0.125, plot::yerrorbar = e1,
                   plot::title = "data", plot::c_black,
                   plot::s_points, plot::pt_fcir, plot::ps_med_small);
    return 0;
}

例.3 カラーマップ、ベクトル、等高線

#include <thread>
#include <OpenADAPT/Plot/Canvas.h>

double calc_r(double x, double y)
{
    return std::sqrt(x * x + y * y);
};
double potential(double x, double y)
{
    double r1 = calc_r(x - 3, y);
    double r2 = calc_r(x + 3, y);
    if (r1 == 0.) r1 = 0.000001;
    if (r2 == 0.) r2 = 0.000001;
    double p1 = 1 / r1;
    double p2 = 3 / r2;
    return p1 - p2;
};
double fieldx(double x, double y)
{
    double f1 = (x - 3) / std::pow(calc_r(x - 3, y), 3);
    double f2 = 3 * (x + 3) / std::pow(calc_r(x + 3, y), 3);
    return f1 - f2;
}
double fieldy(double x, double y)
{
    double f1 = y / std::pow(calc_r(x - 3, y), 3);
    double f2 = 3 * y / std::pow(calc_r(x + 3, y), 3);
    return f1 - f2;
}
int main()
{
    adapt::Matrix<double> m(100, 100);
    std::pair<double, double> xrange = { -9.9, 9.9 };
    std::pair<double, double> yrange = { -9.9, 9.9 };
    for (int iy = -50; iy < 50; ++iy)
    {
        double y = iy * 0.2 + 0.1;
        for (int ix = -50; ix < 50; ++ix)
        {
            double x = ix * 0.2 + 0.1;
            m[ix + 50][iy + 50] = potential(x, y);
        }
    }
    std::vector<double> xfrom(441), yfrom(441), xlen(441), ylen(441);
    std::vector<double> arrowcolor(441);
    for (int iy = -10; iy <= 10; ++iy)
    {
        for (int ix = -10; ix <= 10; ++ix)
        {
            size_t jx = (ix + 10);
            size_t jy = (iy + 10);
            double xlen_ = fieldx(ix, iy);
            double ylen_ = fieldy(ix, iy);
            double len = std::sqrt(xlen_ * xlen_ + ylen_ * ylen_);
            xlen_ = xlen_ / len * 0.8;
            ylen_ = ylen_ / len * 0.8;
            xlen[jy * 21 + jx] = xlen_;
            ylen[jy * 21 + jx] = ylen_;
            xfrom[jy * 21 + jx] = ix - xlen_ / 2.;
            yfrom[jy * 21 + jx] = iy - ylen_ / 2.;
            arrowcolor[jy * 21 + jx] = potential(ix - xlen_ / 2., iy - ylen_ / 2.);
        }
    }

    namespace plot = adapt::plot;
    {
        std::string output_filename = "example_colormap.png";
        adapt::MultiPlot multi(output_filename, 1, 2, 1200, 600);

        adapt::CanvasCM g1(output_filename + ".map_tmp");
        g1.SetTitle("example\\_colormap");
        g1.SetPaletteDefined({ {0, "yellow" }, { 4.5, "red" }, { 5., "black" }, { 5.5, "blue"}, { 10, "cyan" } });
        g1.SetSizeRatio(-1);
        g1.SetXLabel("x");
        g1.SetYLabel("y");
        g1.SetXRange(-10, 10);
        g1.SetYRange(-10, 10);
        g1.SetCBRange(-5, 5);
        g1.PlotColormap(m, xrange, yrange, plot::notitle).
            PlotVectors(xfrom, yfrom, xlen, ylen, plot::notitle, plot::c_white);

        //sleep for a short time to avoid the output image broken by multiplot.
        std::this_thread::sleep_for(std::chrono::milliseconds(300));

        adapt::CanvasCM g2(output_filename + ".cntr_tmp");
        g2.SetTitle("example\\_contour");
        g2.SetPaletteDefined({ {0, "yellow" }, { 4.5, "red" }, { 5., "black" }, { 5.5, "blue"}, { 10, "cyan" } });
        g2.SetSizeRatio(-1);
        g2.SetXLabel("x");
        g2.SetYLabel("y");
        g2.SetXRange(-10, 10);
        g2.SetYRange(-10, 10);
        g2.SetCBRange(-5, 5);
        g2.PlotColormap(m, xrange, yrange, plot::notitle,
                        plot::with_contour, plot::without_surface, plot::variable_cntrcolor,
                        plot::cntrlevels_incremental = { -20., 0.2, 20. }).
            PlotVectors(xfrom, yfrom, xlen, ylen, plot::notitle, plot::variable_color = arrowcolor);
    }

    return 0;
}

例.4 日付ラベル、塗りつぶし

#include <ranges>
#include <random>
#include <ctime>
#include <chrono>
#include <OpenADAPT/Plot/Canvas.h>

int GetTested(int d)
{
    static std::random_device rd;
    static std::mt19937_64 mt(rd());
    double a = d * d * 0.05;
    double b = a * std::pow((std::sin(2 * 3.141592 * d / 120) + 1) / 2, 3) + d;
    return b <= 0 ? 0 : std::poisson_distribution<>(b)(mt);
}
int GetPositive(int t)
{
    static std::random_device rd;
    static std::mt19937_64 mt(rd());
    return t == 0 ? 0 : std::binomial_distribution<>(t, 0.05)(mt);
}

std::pair<int, int> GetDate(int a)
{
    static constexpr std::array<int, 12> x = { 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 };
    int m = 0;
    int d = 0;
    int p = 0;
    for (int i = 0; i < 12; ++i)
    {
        if (a + 1 <= x[i]) { m = i + 1, d = a + 1 - p; break; };
        p = x[i];
    }
    return { m, d };
}

struct Accumulator
{
    int operator()(int t) const { return sum += t; }
    mutable int sum;
};
int main()
{
    std::vector<int> yr(366);
    std::iota(yr.begin(), yr.end(), 0);
    auto x = std::views::all(yr) | std::views::transform([](int a) { auto [m, d] = GetDate(a); return std::format("2020-{}-{}", m, d); });
    auto y = std::views::all(yr) | std::views::transform([](int a) { return GetTested(a); });
    auto y2 = y | std::views::transform([](int t) { return GetPositive(t); });
    auto y3 = y | std::views::transform(Accumulator{});
    auto y4 = y2 | std::views::transform(Accumulator{});

    namespace plot = adapt::plot;
    adapt::Canvas2D g("example_datetime.png");
    g.SetXTicsRotate(-45);
    g.SetTitle("example\\_datetime");
    g.SetYLabel("per day");
    g.SetY2Label("total");
    g.SetKeyTopRight();
    g.SetKeyOpaque();
    g.SetKeyBox();
    g.SetXDataTime("%Y-%m-%d");
    g.SetFormatX("%02m/%02d");
    g.PlotPoints(x, y, plot::title = "tested", plot::s_steps, plot::fillsolid = 0.5).
        PlotPoints(x, y2, plot::title = "positive", plot::s_steps, plot::fillsolid = 0.5).
        PlotLines(x, y3, plot::title = "tested\\_total", plot::ax_x1y2, plot::lw_ex_thick).
        PlotLines(x, y4, plot::title = "positive\\_total", plot::ax_x1y2, plot::lw_ex_thick);

    return 0;
}

メンテナンス再開の経緯

私は2018年頃にADAPT-GPM2の制作を開始し、2019年にはGitHubで公開した。まあ鳴かず飛ばずで、使ってくれた人は数えるほどしかいなかったと思われるが、私個人が必要とする機能を十分に実装したし、満足していた。

そんな折、2020年頃にMatplot++というC++用の本格的なプロットライブラリが登場した。傍目にはかなりしっかり作り込まれ、Examplesも充実しており、これは私も導入できるのではと期待を寄せていた。自作したことはしたが、別に自作したかったわけではなく他に真っ当な選択肢がなかったから仕方なく作ったのであって、もっと優れたサードパーティ製ライブラリが出てきてメンテナンスの手間から開放されるのなら願ったり叶ったりだった。

そして2023年の暮れに本格的に導入し、およそ1年間使い続け、私はMatplot++を捨てることに決めた。……見た目ほど優れたライブラリではなかった。ああいや、ADAPT-GPM2よりは多機能だし便利だと思う。が、私にとっては些か使いにくかった。任意のrangeに対応すると謳っているくせに実質的にはstd::vector<double>しか受け付けない、1000万個くらいのデータ点をプロットすると凄まじく遅い、PNG画像の取り扱いなどに難点がある、多数の画像を出力したい場合にいちいち既存のaxes_handleを個々に削除してfigure_handleを使い回す必要があり手間がかかる、など。細かい不満を挙げていくときりがない。

特に致命的な問題だったrangeについてはIssueを投げてみたものの、開発者はすでに半ばメンテナンスを放棄した状態で、時間がないから無理だと断られてしまった。もちろんOSS開発は個人の善意によって成り立つもので、メンテナンスできない作者を責めるつもりはない。が、不具合だらけなのにメンテナンスされていないライブラリを使い続けられるほどこちらも我慢強くはないのだ。

ちなみに、私は使ったことがないが、sciplotという類似のライブラリも同時期に出てきている。ただしこちらももう、プルリクエストを反映する以上のメンテナンスは3年以上行われていないようである。Matplot++の教訓に乗っ取るのであれば、こちらも導入は厳しいだろう。

そういうわけで、少なくとも自分で使う限りは殆ど不満の出なかった自作ライブラリに戻ってくることにしたのである。

もともとADAPT-GPM2はその名前の通りADAPTの一部として開発したもので、Gnuplotラッパーの部分だけを切り抜いてOSSにしていたのだ。2024年1月にADAPT本体をOSS化したことに伴い、ADAPT-GPM2も切り離して管理する必要がなくなったので、それならばと統合することにした。

せっかくC++20に対応させるならと、若干のコード修正も行った。キーワード引数を使ったオプション指定は、どうせならC++20で対応した指示付き初期化を使おうと思ったのだが、いくつかの難点があり断念した。例えば指示付き初期化は基底クラスのメンバ変数初期化をシンプルに書き下せない。本ライブラリではグラフ描画用オプションをキーワード引数として実装していたのだが、グラフの種類(点、線、塗りつぶしなど)でそれぞれ対応する範囲が微妙に異なり、それを基底-派生の関係で表現していた。が、指示付き初期化では事実上クラスを派生させられないので、オプションの整理が大変になる。また初期化子の順序を守らなければならない点も厄介だった。Gnuplotでオプション指定順序に悩まされた身としては、この仕様は受け入れがたかった(まあコンパイルエラーが出るから明らか、という考え方もできるが……)。このあたりの話は前回の記事に詳しく書いている

このため、キーワード引数は過去に使っていた実装をC++20で若干修正しつつ残すことにした。こちらのほうが遥かに使い勝手が良い、と判断した。結果的にキーワード短縮記法なども実現したので、まあ悪くはなかった。

今後の更新予定としては、一次元/二次元ヒストグラムなどは私自身が頻繁に使うので遠からず実装されるだろう。
その他、当時は色々と拡張性を考慮して設けておいた機能のうち、ほとんど使わなかったものや、ADAPT本体と統合したことで不要になったものなども、切り捨てて整理しているところだ。いずれPythonバインディングを作ることを想定してType erased rangeみたいなものを設けるなど色々工夫していたが、実用していた4年あまりの間、有意義に働くことはほとんどなかった。
これらを少しずつ整備していきたい。仕事に追われているのでいつになるかは分からないが。


  1. 昔の記事の書き直しのようなものなので、以前の記事と似たタイトルを付けたいと思ったのだが、詐欺臭くなってしまった。でも大抵こういう釣りタイトルのほうがPV稼ぎやすいよね。

[C++]C++20/23時代でも独自の名前付き引数は必要だった。

動機

C++20では指示付き初期化が導入された。これは集成体初期化を行う際にメンバ変数名を明示的に指定しつつ初期化する方法で、ありがたいことに、極めて簡単にC++での疑似名前付き引数として利用できる。
ただし指示付き初期化にも難点が多数ある。まず初期化子の順序が必ず宣言順でなければならないことだ。

struct Params
{
    int a = 1;
    double b = 10.;
    std::string c;
}
void func(Params p)
{
    std::cout << std::format("{} {} {}\n", p.a, p.b, p.c);
}
int main()
{
    func({ .a = 2, .c = "abc" });//ok
    func({ .c = "abc", .a = 2 });//error
}

そして、初期化する集成体が基底クラスを持つ場合に、その初期化が非常に面倒である点も腹立たしい。

struct ParamA
{
    int a;
};
struct ParamsB : ParamA
{
    double b;
};

int main()
{
    // 基底クラスは明示的に{ ... }と指定する必要があり、
    // .a = 1, .b = 4のように初期化できない。
    ParamsB p{ { .a = 1 }, 4 };//ok
    ParamsB p{ .a = 1, .b = 4 };//error
}

参照型引数の扱いも厄介である。クラスが参照型メンバ変数を持つとその変数は必ず初期化しなければならないので、「この引数は参照で受け取るが、与えても与えなくてもいい」といった使い方をする場合に困る。デフォルトではstaticメンバ変数などを参照させるというちょっと際どい手段はあるかもしれないが、危なすぎて私はやりたくない。

そんなわけで、自作ライブラリの中で指示付き初期化を導入しようとあれこれ考えるも断念し、結局自前の名前付き引数を継続して使用することにした。もう何年も前、C++14でメタプログラミングを覚え悪戦苦闘していた頃に作り、使い続けてきた機能だ。とはいえ時代は既にC++20/23なので、コンセプトなどを使って一部改修した。昔使っていたものに比べれば幾分シンプルになった。折角なので記事に残しておく。

実装

#ifndef KEYWORD_ARGS_H
#define KEYWORD_ARGS_H

#include <utility>
#include <tuple>
#include <type_traits>
#include <concepts>

template <class> struct AlwaysTrue {};

//任意の型を受け取ることのできるキーワードを作りたい場合、これを与える。
template <template <class> class Concept = AlwaysTrue>
class AnyTypeKeyword {};

template <class Name_, class Type_, class Tag_>
struct KeywordValue
{
    using Name = Name_;
    using Type = Type_;
    using Tag = Tag_;

    constexpr KeywordValue(Type v) : m_value(std::forward<Type>(v)) {}
    constexpr Type GetValue() { return std::forward<Type>(m_value); }
    template <class T>
    constexpr bool Is() const { return std::is_same<T, Type>::value; }
private:
    Type m_value;
};

template <class Name, class Type, class Tag>
struct KeywordName;
template <class Name_, class Type_, class Tag_>
struct KeywordName
{
    using Name = Name_;
    using Type = Type_;
    using Tag = Tag_;
    using Value = KeywordValue<Name, Type, Tag>;

    constexpr KeywordName() {}
    constexpr Value operator=(Type v) const { return Value(std::forward<Type>(v)); }
};
template <class Name_, template <class> class Concept_, class Tag_>
struct KeywordName<Name_, AnyTypeKeyword<Concept_>, Tag_>
{
    using Name = Name_;
    using Tag = Tag_;

    constexpr KeywordName() {}
    template <class Type, class = Concept_<Type>>
    constexpr KeywordValue<Name, Type&&, Tag> operator=(Type&& v) const
    {
        return KeywordValue<Name, Type&&, Tag>(std::forward<Type>(v));
    }
};
template <class Name_, class Tag_>
struct KeywordName<Name_, bool, Tag_>
{
    using Name = Name_;
    using Type = bool;
    using Tag = Tag_;
    using Value = KeywordValue<Name, bool, Tag>;

    //キーワード名インスタンスのみが与えられている場合、trueとして扱う。
    static constexpr bool GetValue() { return true; }
    constexpr Value operator=(bool v) const { return Value(v); }
};

template <template <class...> class Base, class Derived>
struct IsBaseOf
{
    template <class ...U>
    static constexpr std::true_type check(const Base<U...>*);
    static constexpr std::false_type check(const void*);

    static const Derived* d;
public:
    static constexpr bool value = decltype(check(d))::value;
};

template <template <class...> class Base, class Derived>
inline constexpr bool IsBaseOf_v = IsBaseOf<Base, Derived>::value;

template <class T, template <class...> class U>
concept derived_from = IsBaseOf_v<U, T>;

template <class Name>
concept keyword_name = derived_from<std::remove_cvref_t<Name>, KeywordName>;

template <class Option>
concept keyword_value = derived_from<std::remove_cvref_t<Option>, KeywordValue>;
template <class Option>
concept keyword_name_of_bool = keyword_name<Option> && std::same_as<typename std::remove_cvref_t<Option>::Type, bool>;

template <class Option>
concept keyword_arg = keyword_value<Option> || keyword_name_of_bool<Option>;

template <class Option, class ...Tags>
concept keyword_arg_tagged_with = keyword_arg<Option> && (std::same_as<typename std::remove_cvref_t<Option>::Tag, std::remove_cvref_t<Tags>> || ...);

template <class Option, class KeywordName>
concept keyword_arg_named = keyword_name<KeywordName> && keyword_arg<Option> &&
std::same_as<typename std::remove_cvref_t<KeywordName>::Name, typename std::remove_cvref_t<Option>::Name>;

namespace detail
{

class EmptyClass {};
template <class...> struct TypeList {};

template <keyword_name KeywordName>
constexpr bool KeywordExists_impl(KeywordName) { return false; }
template <keyword_name KeywordName, keyword_arg Arg, keyword_arg ...Args>
constexpr bool KeywordExists_impl(KeywordName, Arg&&, [[maybe_unused]] Args&& ...args)
{
    if constexpr (keyword_arg_named<Arg, KeywordName>) return true;
    else return KeywordExists_impl(KeywordName{}, std::forward<Args>(args)...);
}

template <keyword_name KeywordName, class Default>
constexpr decltype(auto) GetKeywordArg_impl(KeywordName, [[maybe_unused]] Default&& dflt)
{
    if constexpr (std::same_as<std::remove_cvref_t<Default>, EmptyClass>) throw std::exception("Default value does not exist.");
    else return std::forward<Default>(dflt);
}
template <keyword_name KeywordName, class Default, keyword_arg Arg, keyword_arg ...Args>
constexpr decltype(auto) GetKeywordArg_impl(KeywordName name, [[maybe_unused]] Default&& dflt, [[maybe_unused]] Arg&& arg, [[maybe_unused]] Args&& ...args)
{
    if constexpr (keyword_arg_named<Arg, KeywordName>) return static_cast<std::remove_cvref_t<Arg>::Type>(arg.GetValue());
    else return GetKeywordArg_impl(name, std::forward<Default>(dflt), std::forward<Args>(args)...);
}

}

template <keyword_name KeywordName, keyword_arg ...Args>
constexpr bool KeywordExists(const KeywordName& name, Args&& ...args)
{
    return detail::KeywordExists_impl(name, std::forward<Args>(args)...);
}

//該当するキーワードから値を取り出して返す。
//同じキーワードが複数与えられている場合、先のもの(左にあるもの)が優先される。
template <keyword_name KeywordName, keyword_arg ...Args>
constexpr decltype(auto) GetKeywordArg(KeywordName name, Args&& ...args)
{
    return detail::GetKeywordArg_impl(name, detail::EmptyClass{}, std::forward<Args>(args)...);
}
template <keyword_name KeywordName, keyword_arg ...Args>
constexpr decltype(auto) GetKeywordArg(KeywordName k, typename KeywordName::Type default_, Args&& ...args)
{
    //該当するキーワードから値を取り出して返す。
    //同じキーワードが複数与えられている場合、先のもの(左にあるもの)が優先される。
    return detail::GetKeywordArg_impl(k, std::forward<typename KeywordName::Type>(default_), std::forward<Args>(args)...);
}

#define DEFINE_KEYWORD_ARG(NAME, TYPE)\
inline constexpr auto NAME = KeywordName<struct _##NAME, TYPE, void>();

#define DEFINE_TAGGED_KEYWORD_ARG(NAME, TYPE, TAG)\
inline constexpr auto NAME = KeywordName<struct _##NAME, TYPE, TAG>();

#endif

使い方はだいたい次のような感じである。

#include <iostream>
#include <vector>
#include <string>
#include "KeywordArgs.h"

namespace args
{

//キーワード引数を定義する。
DEFINE_KEYWORD_ARG(lvec, std::vector<int>&);
DEFINE_KEYWORD_ARG(rvec, std::vector<double>&&);
DEFINE_KEYWORD_ARG(flt, float);
DEFINE_KEYWORD_ARG(str, std::string_view);

//制約付きで任意の型を受け取ることのできるキーワード引数を定義する。
template <class Int> requires std::integral<std::remove_cvref_t<Int>>//完全転送の形で受け取るので、remove_cvref_tを使う。
struct AnyInt {};
DEFINE_KEYWORD_ARG(anyint, AnyTypeKeyword<AnyInt>);//任意の整数型。

//短縮記法。
inline const auto f3 = (flt = 3.0f);
inline const auto sa = (str = "aaaaa");
//inline const auto rv123 = (rvec = std::vector<double>{1, 2, 3});//ダングリング参照に注意。

}

template <keyword_arg ...Args>
void func(Args ...args)
{
    //キーワード引数を受け取る。
    std::vector<int>& lvec = GetKeywordArg(args::lvec, std::forward<Args>(args)...);
    for (auto& i : lvec) std::cout << i << " ";
    std::cout << std::endl;
    for (auto& i : lvec) i = i * 2;
    std::vector<double> rvec = GetKeywordArg(args::rvec, std::forward<Args>(args)...);
    for (auto& i : rvec) std::cout << i << " ";
    std::cout << std::endl;

    //キーワードの有無を確認する。
    if constexpr (KeywordExists(args::flt, args...))
        std::cout << "flt found. ";
    else
        std::cout << "flt not found. default value is used.\n";
    //キーワード引数が与えられていない場合、デフォルト値1.0を使う。
    float flt = GetKeywordArg(args::flt, 1.0f, std::forward<Args>(args)...);
    std::cout << flt << std::endl;

    auto anyint = GetKeywordArg(args::anyint, std::forward<Args>(args)...);
    std::cout << typeid(anyint).name() << ", " << anyint << std::endl;

    std::cout << GetKeywordArg(args::str, std::forward<Args>(args)...) << std::endl;
}

int main()
{
    std::vector<int> lv = { 1, 2, 3 };
    std::vector<double> rv = { 1.1, 2.2, 3.3 };
    func(args::lvec = lv, args::rvec = std::move(rv), args::anyint = 15ll, args::sa);

    for (auto i : lv) std::cout << i << " ";//funcによってlvの要素が2倍されている
    std::cout << std::endl;
    for (auto i : rv) std::cout << i << " ";//rvはムーブされているので空
    std::cout << std::endl;

    return 0;
}

また、キーワード引数にタグを設定し、特定のタグのものだけを受け取るよう範囲調整する機能を設けている。

namespace args
{

struct Param1 {};
struct Param2 {};
DEFINE_TAGGED_KEYWORD_ARG(param1_int, int, Param1);
DEFINE_TAGGED_KEYWORD_ARG(param1_dbl, double, Param1);
DEFINE_TAGGED_KEYWORD_ARG(param2_int, int, Param2);
DEFINE_TAGGED_KEYWORD_ARG(param2_dbl, double, Param2);

struct Param3 {};
DEFINE_TAGGED_KEYWORD_ARG(param3_int, int, Param3);

}

//Param1または2をタグとして持つ引数だけ与えられる。
template <keyword_arg_tagged_with<args::Param1, args::Param2> ...Args>
void func2(Args&& ...args)
{
    std::cout << GetKeywordArg(args::param1_int, std::forward<Args>(args)...) << std::endl;
}

int main_()
{
    func2(args::param1_int = 1);//OK
    //Func2(args::param3_int = 1);//error
    return 0;
}

指示付き初期化と比べると、色々とメリットはある。

  • 順序が任意。
  • タグの継承関係で対応するオプションの範囲を調整できる。
  • 任意の型を受け取ることができる。
  • 参照受け取りに困らない。
  • 短縮記法なども可能(ただしダングリングには注意)。

一方で、実用する上では名前空間で括ることがほぼ必須なので、記述が長たらしくなりがちだし、名前空間を汚染しやすいのもデメリットだ。簡素なプリミティブ型ばかりのパラメータを受け取る場合などは、指示付き初期化のほうがずっと気軽に使えるだろう。

閑話

本機能はGnuplotラッパーの部分改修およびADAPTへの統合のために作り直したものだ。なのであちらのライブラリで必要になった機能しか実装していないし、AnyTypeKeywordなどはかなりいい加減な作りになっている。しかし自分で使う分にはまあ困ることはないだろう。

まあ所詮は、ネット上にゴロゴロ転がっているC++用名前付き引数の亜種に過ぎない。指示付き初期化で満足できない奇特な人の参考になってくれたら幸いである。私は指示付き初期化でもネット上で紹介されている数多の名前付き引数実装でも満足できなかったので自作するしかなかった。C++も悩ましい言語だなぁといつも思う。

[C++][ADAPT]データ分析、処理ライブラリADAPTの更新情報(2)。

ADAPTの更新情報である。本ライブラリを私個人も研究の中で本格的に導入し、その都度欲しくなった機能の追加などを行ってきたので、そのまとめだ。

ADAPTについての説明はこちらへ。
ADAPTのGitHubリポジトリはこちらへ。

条件指定可能な階層関数の追加

ADAPTはgreatest(population / area)のように下層要素を集計した結果を返す階層関数を用意している1ExcelのSUM、MAX関数などに似た機能である。ここにsum_ifgreatest_ifのような条件指定可能な関数を用意した。例えばgreatest_if(population / area, area < 100)としたとき、area < 100を満たすものだけを対象にpopulation / areaの最大値を求めその値を返す。

同様に、isgreatest_ifのようにis関数に対しても条件指定機能を設けた関数を用意している。関数名がよく分からないことになっている気がするが……まあ気にしたら負けだろう。

Extractの高速化

Filterによる条件指定がなく、かつシングルスレッド動作の場合で、Extractの動作を20~40%高速化した。マルチスレッドの場合はそれほど速度は上昇しない(CPU使用率を下げる効果は多少あるかもしれない)、Filter条件指定がある場合も効果は薄い。
とはいえ高速化についてはまだ作業途中で、今後もっと効率化を進めるつもりだ。

ラムダ関数用のtryjoinの追加

連結コンテナはTryJoin関数という下位コンテナの連結を試みる関数を持つが、これをラムダ関数中でも使用できるようにした。ちょっと呼び出し方が特殊で、JoinedContainerオブジェクトcがあるとき、if_(c.tryjoin(1_rank), x, y)のようにコンテナオブジェクトのメンバ関数tryjoin関数を呼ぶことで使用する。動作はTryJoinと同じで、連結可能なら連結したうえでtrueを返し、不可能な場合は連結をせずfalseを返す。

細かなバグ修正

バグは使っているうちに色々と見つかってくるので、その都度修正している。JoinedContainerと階層関数を組み合わせて使用したときに起こるバグ、空要素を持つ場合にTraverserが無限ループを起こす可能性のあるバグなどを修正した。

その他現状報告

何とかSIMDを有効化しようと奮闘している。のだが、階層構造や行指向データ構造とSIMDはあまり相性が良くないことに頭を悩ませている。いやまあ、相性の悪さを理解した上で、それでもこのデータ構造を採用したい背景があったのだが。
一応、不可能ではないことは分かっているし、SIMDによる計算量削減だけでなく仮想関数呼び出しオーバーヘッドの削減にも繋がるので、効果は少なからずあるはずだ。ただ場合によっては、やはり行指向を諦めて列指向の設計を始めるかもしれない。こうなると用途上色々と問題が出てくるかもしれないが、逆に列指向のメリットを押し付けることもできるかもしれない。

何にせよ、実装にはまだ時間がかかりそうだ。


  1. population、areaはそれぞれ市町村の人口と面積とする。