[C++]std::ranges::join_view::beginのconst/非constでの要求の違い。

細かすぎる話のような気がするが、ここで引っかかって丸一日浪費したのでメモっておく。

std::ranges::join_view::begin()はconstか非constかによってrangeに対する要求が変わる。

std::ranges::join_viewに与えたrangeをVとするとき、もしstd::ranges::range_reference_t<const V>が参照型ではなかった場合、std::ranges::join_view::begin() constを呼び出すことが出来ない。非constなstd::ranges::join_view::begin()は問題なく呼び出せる。

通常の状況、例えばstd::vector<std::vector<double>>join_viewに与えた場合を考える。この場合はstd::ranges::range_reference_t<const std::vector<std::vector<double>>>const std::vector<double>&である。つまり参照型であるため問題ない。一方例えば、MyVector<std::vector<double>> myvecのようものがあったとして、*myvec.begin()std::vector<double>の一時オブジェクトを返すような場合、join_view::begin() constは呼び出せない。

理由は、考えてみればシンプルではある。join_viewに与えられたrangeのうち、外側をouter、内側をinnerと呼ぶことにする。このときjoin_view::beginは内部で大雑把に次のような処理をしているはずである。

auto begin() const
{
    const auto& inner = *begin(outer);
    auto it = begin(inner);
    ...
}

ここでもしinnerが何らかの一時オブジェクトであった場合、これはbeginの関数処理を終える時点で破棄されることになる。そうなると、join_viewitが有効であることを保証できなくなる。例えば上述のように*begin(outer)が返すものがstd::vectorの一時オブジェクトであったとすると、itbeginを抜けた時点でダングリング参照となる。

したがって、join_view::begin() constは厄介なことに、outerがrangeのproxy objectを返すような場合、これを受け付けない。私はこの問題に引っかかり、その原因特定と修正のために丸一日を費やした。この時私は自作のN次元配列クラスをjoin_viewに与えており、*begin(outer)がinner rangeのproxy objectを返すように設計していた。もちろん普通に使用する限りダングリングを起こすことはないのだが、join_viewは許してくれなかった。rangeやiteratorではproxyを使うのは日常茶飯事なので、まさかこんな罠が隠れているとは思いもしなかった。結局私はjoin_viewを使わず、フラットに走査するための新たなrangeとiteratorを用意することにした1

他方、非constのjoin_view::begin()の方は、参照型でない場合も許している。これはNon-propagating cacheという仕様により、一時オブジェクトとして返されたinnerを自身の内に保存しておくことができ、ダングリングを起こさないことが保証されているためである。言い換えれば、constのjoin_view::begin()はそのconst性ゆえにinnerを保存することが出来ないため、呼び出しが禁止されている。

C++難しすぎる。

参考

https://en.cppreference.com/cpp/ranges/join_view/begin
https://en.cppreference.com/cpp/ranges/non-propagating-cache
https://stackoverflow.com/questions/78468803/why-cant-i-pass-a-stdviewsjoin-by-const-reference


  1. 既存のコードを流用すれば数行書き加えるだけで実現できることに気づいたので、この方がjoin_viewに固執し回避手段を講じるよりも圧倒的に楽だった。

[C++][ADAPT]見た目の改善とヒストグラム多機能化。Gnuplotライブラリ更新(3)。

最近自分の中で出番が多い本ライブラリ(まあプロット機能だけでなくADAPTのデータ分析全般は日ごろから使い倒しているが)。特にヒストグラム関係はしょっちゅう使うので、色々と機能が足されている。何を更新したのか忘れてしまいそうなので、このあたりでまとめておこうと思う。

プロット機能の詳細はこちらへ
GitHubリポジトリはこちらへ

デフォルトの解像度の拡大

私がGnuplotに抱えていた大きな不満点の一つは、デフォルトの解像度が低く文字が荒いこと、単純に解像度を引き上げたとしても文字や線の太さが変わらないので全く奇麗にならないことだ。今までは解像度を800x600などに制限することでやりくりしていたが、あまりの文字の汚さに嫌気がさし、見た目を整えることにした。
今、出力ファイル形式をpngにした場合のデフォルトの解像度は1200x900になっている。線の太さ、点の大きさ、タイトルやラベルのフォントの大きさなども整えたし、解像度設定に合わせて自動調整するようにしたので、幾分奇麗になっているとは思う。ただGnuplotはこのあたりターミナルごとに挙動がころころ変わり非常に調整が大変で、正常に動かない部分もあると思うので、こんなライブラリにユーザーがいるかどうかは不明だが、もし問題があったら報告してほしい。
デフォルトのフォントは互換性などいろいろと悩んだ末にArialに戻してあるが、私は近年普及しつつあるNoto Serifを使用している。過度に格式高いこともなく美しさと視認性が両立しているバランスの良いフォントに感じられて、色々試した中で最も気に入っている。ADAPT_PLOT_FONTという環境変数の値を任意のフォントに変更するか、adapt::SetPlotFontName("...")とすることでデフォルトのフォントが切り替わるので、好みのものがあれば設定しても良い。

なお実行時、Gnuplotがproblem determining pango font metricsのような警告を発し、文字が適切に描画されないことがあるかもしれない。これは本ライブラリではなくGnuplot側(もしくはその文字描画バックエンドであるCairoかPango)の不具合で、フォントの大きさを一定以上に設定すると発生する。ライブラリの側で解決する術はないが、対症療法は存在し、環境変数PANGOCAIRO_BACKENDの値をfcに設定することで解消される。

累積相対度数分布

PlotHistogram関数にplot::cumulまたはplot::inv_cumulというオプションを与えることで、プロットが累積相対度数分布に切り替わる。相対ではない累積度数にもできるようにするかちょっと悩むところだったが、私が現時点で必要としていないので未実装だ。
コード例はこちらを参照

積み上げヒストグラム

PlotHistogram関数に対してplot::stackオプションを与えることで、plot::stackオプションが与えられたすべてのデータを対象にヒストグラムを積み上げ表示する。 なおx軸の最小、最大、ビン数は最初のPlotHistogram関数で指定すれば2度目以降は省略可能である。というか、ここで整合しない値を与えると結果がおかしなことになるので省略することを推奨する。
コード例はこちらを参照。 なおplot::stackを有効にした場合はヒストグラムの塗りつぶしが勝手に行われるのだが、このとき後述するPlotFilledCurvesを呼び出している関係で、各プロットの色を明示することが望ましい。

重み付けされたヒストグラム

PlotHistogram関数に対してplot::weightオプションによりデータごとの重みを与えられるようにした。
重みを考慮した統計誤差のエラーバー描画も可能ではあるが、重み付きとなると正規分布で近似して計算せざるを得ない。そのためplot::he_normalは使用可能だがplot::he_poissonには非対応である。コード例はこちらを参照

PlotFilledCurvesの拡張と整理

今までwith filledcurvesPlotFilledCurveswith boxeswith fillstepsによる塗りつぶしはPlotPointsで呼び出すというように機能が十分に分離できていなかったが、これらをPlotFilledCurvesに統合した。plot::s_linesplot::s_boxesplot::s_stepsいずれかをオプションとして与えるとそれに応じた形式でプロットされる。
また比較的新しいGnuplotではfilledcurvesfillstepsにおいてボーダーが描画されないという腹立たしい仕様変更があったのだが、これをライブラリ側で吸収し描画するようにした。ボーダー不要の場合はオプションにplot::noborderを与えれば良い。

なお、ボーダーを描画するときは明示的にplot::c_redなどのように色を指定することを推奨する。現時点では色を明示しないと塗りつぶしの色とボーダーの色とがちぐはぐになってしまうためだ。ただこれを修正するにはGnuplotの仕様に抗って色々と設計修正する必要が生じて大変なので、今回は見送った。

色の追加

今までred/magenta/blue/cyan/green/yellowの6系統にdark/lightを加えた18色、さらに黒白灰色合わせて5色の名前を設けていたが、もっと中間色を使いたくなったので大量に用意した。現在は上記23色に加え、色相を15度ずつずらして生成した24色をベースにそれぞれlight/dark/mutedを加えた計96色が新たに使用可能になった。各色と名前の対応は以下の図のとおりである。 こういう色について、美的感覚の強い人は美しい色を選定して名前を与えたりするのかもしれないが、私はOklch色空間でLightness、Chroma、Hueを少しずつ変化させながら機械的に生成した。というより、そのような規則的に並ぶ色の一覧が欲しかったため用意したというほうが正しい。最初HSVで試したものの、あれは人間の色覚とはだいぶかけ離れた定式化であることを知った。実際にHSVで規則的に生成した色を使ってみると明るさにかなりの違いがあり、視認性に難が生じたのだ。とはいえOklchでも容易にsRGBの色域外に飛び出してしまうので調整が難しく、特にChromaがHueによっては全く表現できない問題はもっと調整を加えるかかなり悩んだ(上記画像でも青~緑のあたりは潰れてしまっている)。現在のカラーパレットもベストではないと思う。将来的にさらに調整するかもしれない。

ちなみにOklch色空間の計算にはsoreja様1開発のColorMというC++ライブラリを利用させてもらった。シングルヘッダで使いやすく、良いライブラリだった。将来的に3D可視化ツールを作成するときは色空間ごとの計算に導入させてもらうとRGBやHSV以外にも指定できるようになって面白いかもしれない。高度な色空間ほど直感性がなくなるので実用に値するかは何とも言えないが。

ちょっと愚痴

Gnuplotライブラリとしてはかつてないほど苦労した更新だった気がする。任意の配列に対応させるために動的ジェネリクスのギミックを盛り込んだ時も大変だったが、あれはC++との格闘だった。こちらはGnuplotの仕様(もしかするとバグ)との戦いだった。

例えば解像度の引き上げに関しては、ターミナルがpngcairoの場合とpdfcairoの場合とでは同じ指定をしても見た目が合致しなかったりして、暗中模索をひたすら続けるしかなかった。線の太さ、点の大きさが不自然になったり、破線の間隔が異常なことになったり、ドキュメントにあるオプションを指定しても効果を得られなかったりと、挙動が意味不明なのである。今の調整も正直本当に正しく設定できているのか理解しきれていない部分が多く、不安が尽きない。

PlotFilledCurvesについても非常に悩む更新だった。元々はPlotFilledCurvesという関数そのものを捨てて、PlotPointsに統合する方向で検討していたのだが、やめた。原因は、Gnuplot5.1か5.2の頃に行われたと思われる仕様変更だ。現在のwith filledcurveswith fillstepsは、closedモードでない限りボーダーを描画しないのである。塗りつぶしの部分だけが描画され、境界部分の線はどう設定しても一切現れない。5.0の頃はちゃんと描画されていたのでてっきりバグだと思っていたが、ドキュメントをしっかり読んでみると書かれていた。私にとっては激怒するレベルの悪質な仕様変更であった。
今回、上記の積み上げヒストグラムで塗りつぶしを行うことを考えた際、ボーダーがないせいで見た目が非常に陳腐になってしまい、どうしようかと悩んだ。そうこうするうちに上記のclosedモード以外での仕様変更に気づき、ならばいっそこちらで吸収してしまえと思い、PlotFilledCurves内で明確にボーダーを描画する機能を組み込むことにした。PlotFilledCurvesは内部でwith filledcurvesなどによる塗りつぶしとwith linesなどによるボーダー描画の両方を実行しているのである。この点でPlotFilledCurvesには存在意義が生まれ、消滅を免れた。

歴史あるツールは往々にして解決されないバグめいた挙動を抱え込む。長年開発された巨大なツールはかゆいところに手が届く(が、そのためには莫大な学習コストが必要)。
新しいツールが登場するときは、たいていそれまでのデファクトスタンダードが高機能、複雑化、雑多化しすぎて意味不明になり、シンプルなものを求めた人たちが最新のツールに引っ越していく、という現象が共通して起きているように思う。私はC++というどう考えてもデータ分析に不向きな言語で奮闘している都合上Gnuplotに頼る他ないのだが、本音を言えばもっと便利なツールが出てきてほしいとずっと願っている2

まあC++でデータ分析、可視化を仕様などという奇天烈な人間が滅多に存在せず、いたとしてもROOTやApache Arrowなどを選択するであろう現在、私のライブラリが果たして世間にとって有益なのかは不明であるが、世間がなんと言おうと少なくとも私個人にとっては有益なので、まだしばらく開発を継続するつもりだ。


  1. どうも覚えのある名前だと思ったら2022年のC++ AdCに投稿していらっしゃった。
  2. Matplot++を見つけたときは救世主だと思ったのだが、今となっては……。

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

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

文字列からラムダ関数へ変換する機能の実装

Rttiのラムダ関数に限定されるが、文字列をパースしてラムダ関数に変換するParse関数を用意した。Rttiは静的型情報が削除されているので、こんな芸当も可能だ。というより、こんな芸当を最終的に実現することが、わざわざC++でありながら動的型のラムダ関数なんてものを用意していた理由の一つである。ADAPTの機能を何らかのアプリケーションに組み込む際に、ラムダ関数を外部から文字列パラメータで与えられるようにすることが目的だ。

DTree t;
//...tへのデータ構造定義やデータ格納処理
using Lambda = eval::RttiFuncNode<DTree>;
Lambda lambda1 = Parse(t, "a + mean_if(hypot(b, c), d % 2 == 0)");
Lambda lambda2 = Parse(t, "a - a.at(pos2() - 1i64)");
//上記は以下と同等。
//ADAPT_GET_PLACEHOLDERS(t, a, b, c, d);
//Lambda lambda1 = a + mean_if(hypot(b, c), d % 2 == 0);
//Lambda lambda2 = a - a.at(t.pos2() - (int64_t)1);
Bpos bpos{ 1, 4, 3 };
double res = lambda(t, bpos).f64();

文字列式ラムダ関数の文法は通常の書き方とほとんど同じだが、少し異なるところがある。

  • フィールドへのアクセスは通常ならプレースホルダを用いるが、文字列式ラムダ関数の場合はコンテナクラスの構造定義時に与えた名前をそのまま指定する。
  • 連結コンテナ用は未実装。技術的に可能だが色々と調整が必要なので後回しにした。
  • ユーザー定義関数は現時点で非対応。
  • コンテナのメンバ関数として実装される関数(pos、size)は、メンバ関数としてではなく単なる一般関数のように呼び出す。
  • 整数、浮動小数点リテラルはi16、f32のような接尾辞を付与することで型を調整できる。デフォルトではi32、f64になる。

なお、本機能はラムダ関数の保つ機能のほとんどすべてが組み込まれている関係で、ビルドが非常に重く、吐き出されるバイナリが巨大である。そのためこれの使い方は下記の選択肢を用意している。

使い方1. ヘッダオンリーライブラリとして使う

<OpenADAPT/Parser_impl.h>をincludeするだけでよい。ただしビルドは極めて遅いし重い。Ryzen 7 7700X + メモリ32GBのPC上でMSVCを用いてビルドしたときは何とかぎりぎり通る程度の状態だった。GCCだと失敗するかもしれない。

使い方2. 事前にビルドする

OpenADAPTのビルド時、cmakeコマンドに-DENABLE_PREBUILT_PARSER=ONというオプションを与えることで、DTree/DTable/DHistのためのParse関数を事前にビルドすることができる。-DPREBUILT_PARSER_TARGET=DTree;DHistのように、必要なコンテナの種類を指定してもよい。
この方法を取った場合、多数のソースコードにコンパイル単位を分割した上でビルドするので、標準的なPCでも時間はかかるがビルド可能な程度になっているはずだ。
出力ファイルが中間生成物を合わせて数GBに達してしまうので、サイズを削減したい場合は-DBUILD_SHARED_LIBS=ONとして共有ライブラリ化することも検討されたい。

これを使用する場合は、<OpenADAPT/Parser.h>をincludeしつつ、各ビルド成果物へ静的/動的にリンクすればよい。もしMSVCを使用しており、かつ-DBUILD_SHARED_LIBS=ONにより共有ライブラリとしてビルドした場合、includeよりも前に#define ADAPT_DLL_IMPORTと定義しておくことを推奨する1

CMakeから使用する場合は以下のようにしても良い。

find_package(OpenADAPT REQUIRED)
target_link_libraries(myproj PRIVATE OpenADAPT::Parser)

関数名整理

ラムダ関数内で使用できるsizecountという階層関数であるが、名前と実際の挙動との対応を整理するためにそれぞれcountallcount_ifに変更した。従来の名前はしばらくdeprecatedとして使用可能にしておくが、文字列式ラムダ関数では新しい名前しか使えない。
本当はcountallではなくcountにしたかったが、元々のcount関数と被ってしまうのは非常にまずいのでこのようにしている。とはいえ、既に後述する新しいsize関数があるので出番は多くないだろう。

ラムダ関数内で使用するsizepos関数の追加

こちらのsize関数はコンテナクラスのメンバとして呼び出すsizeである。指定された要素に属している子要素数を返す。 これは殆どの場合、階層関数のcountall(上述のとおりsizeから名前を変更した)に同じ階層のフィールドを与えた場合と同等の結果を返す。

DTree t;
auto lambda1 = t.size(2_layer);//現在指し示している1層要素に所属する2層要素の数を返す。

ADAPT_GET_PLACEHOLDERS(t, fld_layer2);
assert(fld_layer2.GetLayer() == 2_layer);
auto lambda2 = countall(fld_layer2);//指定された1層要素に属す全2層要素のfld_layer2に対してアクセスを試み、アクセスに成功した数を返す。
//lambda1とlambda2は計算過程が異なるが、殆どの場合同じ結果を返す。

違いは速度と、アクセス不能な何かがある場合の挙動、そして連結コンテナで未実装である点だ。
* 階層関数のsize関数は律儀に一つ一つの要素で引数部分の計算を試みながら数えるので動作が遅い。こちらはいちいち数えず単にstd::vector::sizeのように範囲を計算するだけなので高速である。
* 階層関数のsize関数は、引数として与えられた部分の計算を試み、何らかの理由でアクセスに失敗したり計算結果が不正となったりした場合にはその要素をカウントしないというちょっと特殊な振る舞いがある2。新size関数は単にコンテナの要素数を参照して返すだけなのでアクセスという概念がなく、そのような計算を実行できない。
* 新size関数は連結コンテナでは使用できない。というのも、挙動が直感とは異なるものになりかねないため3

pos関数は現在位置を返す関数群である。

Bpos b{ 5, 10, 15 };
auto pos_layer0 = t.pos0();
auto pos_layer1 = t.pos1();
auto pos_layer2 = t.pos2();
//t.pos(N_layer)はt.posN()に等しい。
int64_t p0 = pos_layer0(t, b).i64(); // 5
int64_t p1 = pos_layer1(t, b).i64(); // 10
int64_t p2 = pos_layer2(t, b).i64(); // 15

ToVectorExtractなどのrange conversionによって一括操作する場合に現在位置を参照する手段が必要になることが往々にしてあったため、今更ながら設けた。

基本インデックス型をuint32_tからint64_tへ変更

もともとインデックス型はメモリ使用量削減のために歴史的に32bitとなっており、少しでも上限値を増やすためにunsignedにしていたのだが、ADAPTのフィールドにはunsigned型が存在しないという大きな矛盾点がありどうするか悩んでいた。最近は我々が扱うデータサイズも32bitの範囲を超えつつあることも踏まえ、今回、思い切って64bitに拡大することにした。

BinJointの追加

SHist/DHistの特定のビンに連結する方法。原理的にはKeyJointでも代用可能だが、Histの0層要素に連結する場合はこちらのほうが呼び出しがシンプルである。

//生徒の後期期末試験(exam == 3)の数学の点数をヒストグラム化
ADAPT_GET_PLACEHOLDERS(*m_tree, class_, number, name, exam, math, english);
auto tree = *m_tree | Filter(exam == 3) | ADAPT_EXTRACT(class_, number, name, math);
auto hist = *m_tree | Filter(exam == 3) | ADAPT_HIST(cast_f64(math).named("math"), 5., class_, number, name);
//treeの2層(試験成績層)とhistの0層(ヒストグラムのビン)を連結
auto jt = Join(tree, 2_layer, 0_layer, hist);
auto [class_r0, number_r0, name_r0, math_r0] = jt.GetPlaceholders<0>("class_", "number", "name", "math");
auto [class_r1, number_r1, name_r1, math_r1] = jt.GetPlaceholders<1>("class_", "number", "name", "math");
//連結対象にはtree側の数学成績が収まるであろうビンを指定
jt.SetBinJoint<1_rank>(cast_i32(math_r0 / 5.));
//数学の点数が一致する生徒の一覧を表示
jt | Filter(math_r0 == math_r1, !(name_r0 == name_r1 && number_r0 == number_r1)) |
Show(class_r0, number_r0, name_r0, math_r0, class_r1, number_r1, name_r1, cast_i32(math_r1));

試験的なモジュールのサポート

遊び半分で、本ライブラリ(Parser部分を除く)をモジュールとして使用できるようにした。ただし、ADAPTをCMakeでインストールする際に-ENABLE_MODULE=ONとオプションを与える必要がある。さらに現時点でビルド可能なのはClang>=20とGCC>=15のみで、MSVCでは現状ビルド不能である。"sorry: not yet implemented"て……。

find_package(OpenADAPT REQUIRED)
target_link_libraries(myproj PRIVATE OpenADAPT::Module)
//このあたりのヘッダは事前にincludeする必要がある。
#include <format>
#include <iostream>
#include <vector>
#include <string>
#include <cassert>
#include <cmath>
#include <complex>
#include <ranges>
#include <random>
#include <filesystem>
#include <thread>
//モジュールからはマクロをimportできないので、独立にincludeする。
#include <OpenADAPT/Macros.h>
import adapt;

ものは試しと色々頑張って使えるようにしてみたものの、私にはほぼ何の恩恵もなかったので今後サポートし続けるかは不明である。

与太話

文字列からラムダ関数を作る機能って誰が使うの、と甚だ疑問に思うような更新内容だが、私が使うのである。ADAPT v1以前にはそもそもこちらの文字列式から生成するラムダ関数機能しかなかった。というのもADAPTは本来、パラメータファイルなどから文字列情報として計算式を受け取ることを念頭に開発されていたのだ。これはADAPTが昔私が開発していたいくつかの研究用GUIアプリケーションのベースライブラリとなっており、JSONないしYAMLファイルで挙動を記述する必要があったので、そのような設計にならざるを得なかったためである。
OpenADAPTの開発に当たっては、それらの研究用アプリケーションは概ね安定動作し喫緊の開発課題でなくなっていたことで、一旦文字列からのラムダ関数機能は捨て去ることにして、パーサーを端折った実装にした。データ分析ライブラリとしてはこちらのほうが使い勝手が良かったのはまあ、うん。
ただ、諸般の事情でそろそろ上記研究用アプリケーションをアップデートしたいと思い始め、そのためにはOpenADAPTにも文字列式パーサーを実装しなくてはならず、二年越しに重い腰を上げたのである。

この研究用アプリケーションのうち一つは3Dデータビューアだ。主として我々が使用する検出器の情報を3Dで可視化するために作成したGUIアプリケーションであるが、一応どのようなデータでも扱える汎用設計になっており、その肝となっていたのがこの文字列式パーサーだった。これにより、生データをどのような三次元情報に変換するかをパラメータとして記述させていたのだ。
何もかもが思惑通りに進んだ場合、いずれこのツールもオープンソースで公開するかもしれない。それこそ誰が使うの、という話にはなってしまうけれども。

ちなみに今回、パーサーのコードの大半はGitHub CopilotのAgentモードを使用して生成させた。私もAIによるコーディングは色々と試してきたのだが今の今まで上手く行った試しがなく、単純な反復コードを予測させる以外に活用することはほとんどなかった。しかしAgentモードでは自身でテストプログラムを作成、ビルド、実行、エラー修正を反復してくれるようになり大幅に精度が向上したので、これなら実用的だと思い取り入れてみることにした。
尤も、ある程度コードが複雑化すると精度は頭打ちになった。こちらが作れといった機能を作らず保留したり、一度修正させた箇所をわざわざ元の愚劣なコードに戻したり、ライブラリの構造を理解しきれず意味不明な呼び出しを試みたり、過去の記憶をすっかり忘却して同じミスを何度も何度も繰り返したり、速度以外は人間の足元にも及ばない状態ではあった。よって、精度が頭打ちになったあたりでAIに修正させるのを止め、この時点での出力を叩き台として私が大幅な修正を施した。

現時点ではそれほど大規模でないコードの叩き台を作らせるくらいの役割が精一杯であろう。一方で自分では思いつかなかった効果的な実装を提案してくれる場合もあるなど、時間短縮以上のメリットも感じられた。凄まじく仕事が早く思考がフラットである代わりに理解力と記憶力が極めて悪い、アンバランスな部下ができたと思えば、使いようによっては大幅な作業効率化が果たせそうではあった。

……Gitの履歴やGitHubのContributorsにCopilotが載ってしまうのは、ちょっと気持ち悪いなぁと思わなくもないけれども、現状解決方法がなさそうだったので諦めることにした。まあ実際に貢献してくれているのだから、人ではないという理由で排除するのはよくないかもしれない。いや別にAIに人格を認めたりする気はないのだが。


  1. キーワード__declspec(dllimport)を有効化するためのマクロ。
  2. 尤もcount_if関数などで事足りているので、私自身活用したことは多くない。
  3. 厳密に言えば、連結していないDTree等とは振る舞いが一致しない状況が出てくるので、実装をためらっている。まあ現時点ですでに.atなどで異なる振る舞いをしているので、今更かもしれないが……。