嗚呼、帰りたい

プログラミングのことから日常のことまで。いわゆるごった煮というものだな。

GCCのプラグインを作る

この記事はあくあたん工房 Advent Calendar 2024の7日目の記事です。関連記事である「gcc-time-trace-pluginを作る(14日投稿予定)」もよろしければご覧ください。

はじめに

GCCにはプラグイン機能が搭載されており、プラグインを通して任意のコードを実行させることができます。GCCはプラグイン向けにGCC自体を構成するソースコードのヘッダを公開しているため、プラグインからはGCCの内部のデータ構造を概ね自由に読み取ったり変更したりすることができます。

前述の性質のため、GCCのプラグインの作り方について詳細に説明することは、すなわちGCC自体の仕組みについて詳細に説明することと同義であり、本記事で手に負える範疇から逸脱してしまいます。そこで本記事では極めて基本的なプラグインの作り方と、プラグインを作り込んでいく際に助けとなるような情報を紹介します。

単純なプラグインを作る

なにはともあれとにかく動作するGCCのプラグインを作ってみましょう。とても簡単です。

環境構築

まずはGCCとプラグイン用のヘッダをインストールしましょう。プラグイン用ヘッダはapt系なら gcc-<major version>-plugin-dev 1、dnf系なら gcc-plugin-devel 2 というパッケージ名でインストールできます。適宜それっぽい名前で検索してみてください。pacman系ならGCCと一緒についてくる3ようです。

私が使用している環境であるUbuntu24.04の場合は次のコマンドでインストールできます。

sudo apt install g++ gcc-13-plugin-dev

プラグイン作成

次に以下のプラグイン本体のコードを plugin.cpp というファイルに保存しましょう。コードの解説は後でします。

#include <iostream>

#include <gcc-plugin.h>

int plugin_is_GPL_compatible;

int plugin_init(plugin_name_args *args, plugin_gcc_version *version) {
    std::cout << "hello from plugin" << std::endl;
    return 0;
}

このコードを次のコマンドでコンパイルします。これでプラグインが plugin.so という名前でビルドされます。

g++ plugin.cpp -I"$(g++ -print-file-name=plugin)/include" -shared -fPIC -o plugin.so

動作確認

プラグインの動作確認用に以下のような適当なコードを test.cpp として用意します。

#include <iostream>

int main() {
    std::cout << "test" << std::endl;
}

次のコマンドを叩いてプラグインが動作するか確認しましょう。

g++ -fplugin=./plugin.so test.cpp

するとコンパイル時に次のような出力がされます。

hello from plugin

これでプラグインが動作していることが確認できました。特に面倒なビルドの設定などもなく、簡単に作ることができましたね。

コードの解説

プラグイン本体のコードをもう一度掲載します。

#include <iostream>

#include <gcc-plugin.h>

int plugin_is_GPL_compatible;

int plugin_init(plugin_name_args *args, plugin_gcc_version *version) {
    std::cout << "hello from plugin" << std::endl;
    return 0;
}

各部分について解説しましょう。

#include <gcc-plugin.h>

GCCプラグインを作る際に必要となるヘッダをincludeしています。

int plugin_is_GPL_compatible;

plugin_is_GPL_compatible という名前の変数を定義しています。初期化されている必要はありませんが、この変数を定義しておかないとプラグインのコンパイルがそもそも通りません。

この変数の名前からも推測できる通り、プラグインはGPLと互換のライセンス4で公開されることが期待されています5。

int plugin_init(plugin_name_args *args, plugin_gcc_version *version) {
    std::cout << "hello from plugin" << std::endl;
    return 0;
}

GCCがプラグインを初期化する際に呼び出される関数です。名前やシグネチャを別のものに変えたりはできません。プラグインはこの関数の内部でGCCに対してコールバックなどの設定を行います。今回はシンプルなプラグインなので単純に標準出力するだけにしてあります。

プラグインを作り込む

前のセクションではプラグイン作成のとっかかりとなる「とりあえず動かしてみる」ということをやってみました。実際に見せたように、ここまでなら簡単にできます。一方で、作成したプラグインに意味のあることをやらせようとすると、とたんに実装の難易度が跳ね上がります。そこでこのセクションでは、GCCのプラグインを作り込んでいくときにどんなドキュメントを読めばよいかを紹介します。

GNU Compiler Collection (GCC) Internals

https://gcc.gnu.org/onlinedocs/gccint/

GCCの内部についてGCC公式が提供しているドキュメントです。GCCの内部について100%完璧に記述できているわけではありませんが、俯瞰して概略を把握する上で大いに役立ちます。プラグイン機能に関する説明も載っているので、プラグインを作る際は以下のページから目を通していくと良いでしょう。

https://gcc.gnu.org/onlinedocs/gccint/Plugins.html

gcc-python-plugin

https://github.com/davidmalcolm/gcc-python-plugin/ https://gcc-python-plugin.readthedocs.io/en/latest/index.html

GCCの内部をPythonで操作するプラグインです。既存のGCCプラグインの中では最もよく整備されたプラグインだと思われます。GCCの内部をプラグインから操作する上で、具体的にどんなコードを書けばよいのかが分からないような状況ではgcc-python-pluginのコードが大いに参考にできるでしょう。また、プラグインを使うとどんなことができるのか、を知る上でも貴重な資料となります。

gcc-mirror/gcc

https://github.com/gcc-mirror/gcc

GCC自体のソースコードです。当然ながらGCCの内部の仕組みについてはコードを読めばある程度分かります。ただ実際に読むならGCC Internalsのドキュメントを片手に用意した上で覚悟を決めてからやりましょう。かなり独特なコードの書き方がされている箇所があり、ノーヒントで読むには難易度がかなり高いと思います。少なくとも私はつらかったです。ええ。

その他有益かもしれない情報

GCC Bugzilla

https://gcc.gnu.org/bugzilla/

プラグインを作りこむと必然的にGCCの奥深くまでひっかきまわすことになるので、たまにではありますがGCCの怪しい挙動を見つけることがあります。そういったときはBugzillaでどのような対処がなされているのか検索してみると役に立つかもしれないし、あなたが見つけたバグを報告するとコミュニティにとって有益かもしれません。

gcc-time-trace-plugin

https://github.com/shouth/gcc-time-trace-plugin

私が作ったGCCの時間計測用プラグインです。clangの -ftime-trace がGCCにも欲しかったので自作しました。詳細については14日の記事で紹介します。もしかするとプラグインを作るうえでコードとか参考になるかもしれないし、ならないかもしれません。Discussionsも開けてあるので、質問などあれば私の出来る範囲で回答します。お気軽にどうぞ。

おわりに

この記事では基本的なGCCプラグインの作り方と、プラグインをつくるときに有益な情報について紹介しました。私はGCCプラグインを一度作り込んだことがあるのですが、ネット上にとにかく情報が少なく、実現したい機能を実装するのに苦労しました。その数少ない情報についても自分がやりたいこととは内容がズレていてあまり役に立たないことも多かったため、本記事では具体的にプラグインを作り込む部分にはどういった情報源を参照すればよいかという方面にフォーカスを当てました。本記事が誰かの役に立てれば幸いです。


  1. https://packages.debian.org/sid/devel/gcc-13-plugin-dev↩
  2. https://packages.fedoraproject.org/pkgs/gcc/gcc-plugin-devel/↩
  3. https://archlinux.org/packages/core/x86_64/gcc/↩
  4. https://www.gnu.org/licenses/license-list.en.html↩
  5. GPL互換のライセンスに設定する必要性が法的に生じる状況は、あなたが書いたGCCプラグインをビルドして得られたバイナリを公開して配布する場合などに限られると思われるため「期待されている」と表現した↩

レイトレーシングを実装した話

この記事はあくあたん工房 Advent Calendar 2023の9日目の記事です。8日目はトカゲ教授こと水野先生によるヒョウモントカゲモドキの孵卵器を自作するでした。

はじめに

こんにちは、茶葉亭です。アドカレとしてちょうどよいネタを探していたところ、しばらく積んでたRay Tracing in One Weekendを思い出したので、実際に手を動かして実装してみることにしました。参考元では実装にC++を用いていますが、個人的にRustで本格的にコードを書いてみたかったこともあり、今回はRustで実装します。

コードはこちらのリポジトリに置いていますのでよろしければご覧ください。

github.com

続きを読む

コンパイル時brainfuckインタプリタを作った話

この記事はあくあたん工房 Advent Calendar 2021の12日目の記事です

なおこの記事が執筆されたのは12月31日です

大学の必修の課題が重かったんだ...許しておくれ...
なお年内に間に合わせるために色々妥協したのでクオリティについてはお察しください

C++のコンパイル時計算

C++は広く知られた言語ですが,その機能の一つにコンパイル時計算というものがあります.読んで字のごとく,コンパイルの最中に計算する機能です.この記事はコンパイル時計算を説明する記事ではないので詳細な説明は省きますが,例えば次のような関数があったとして

// C++14
constexpr int fib(int n) {
    if (n == 0) return 0;
    if (n == 1) return 1;
    return fib(n - 1) + fib(n - 2);
}

次のように変数に代入したりすると

constexpr int a = fib(10);

コンパイラによって生成されたバイナリでは fib(10) の呼び出しが無くなり,その計算結果である 55 が直接埋め込まれます.

さて,このコンパイル時計算を使うと面白そうなことができそうな気がしますね.ということでコンパイル時にbrainfuckを実行するプログラムを作ってみました.

コンパイル時brainfuckインタプリタ

今回はC++17を使ってプログラムを作りました.まずはプログラム全体をお見せしましょう.

ちょっと長いので折り畳み

brainfuck::interpreter<InputPtr, ProgramPtr> がその名の通りインタプリタとなっていて,第一引数の InputPtr に入力文字列へのポインタを,第二引数の ProgramPtr にプログラムの文字列へのポインタを受け取ります.私の理解が正しければ,template引数に課せられた制約の都合上,これらの引数はグローバル変数として宣言されている必要があります.

brainfuck::interpreter<InputPtr, ProgramPtr> は result に実行結果を,output に出力用の uint8_t 配列を格納します.それぞれについて詳しく見ていきましょう.

result

C++17ではconstexprの文脈でラムダ式を使うことができるのでその機能を活用しています.また,C++17から導入された std::variant (型安全なunionみたいなもの) を使って,result に出力結果かエラーコードを格納しています.出力についてコンパイル時には標準出力を利用することができないので,代わりに特定の長さで配列を確保しておいて順次そこに書き込むようにしており,エラーについてもコンパイル時に標準出力を利用できない都合上エラーコードを返すような設計としています.他に特筆するべき部分はなく,brainfuckプログラムが適格か確認したり,一個一個コマンドを解釈して実行していたりするぐらいです.

output

output は生成された result を基に,出力の余分な部分を切り詰めたり,エラーコード別にコンパイルエラーを出す処理を行ったりします.ある意味コンパイル時計算の威力が最も発揮される部分かもしれません.

static constexpr inline auto output = [] {
    if constexpr (result.index() == 0) {
        constexpr auto buf = std::get<0>(result);
        std::array<uint8_t, buf.second> ret = { };
        for (size_t i = 0; i < buf.second; i++) {
            ret[i] = buf.first[i];
        }
        return ret;
    } else {
        constexpr auto error = std::get<1>(result);
        if constexpr (error == error_reason::MISMATCHING_SQUARE_PARENTHESIS) {
            static_assert(false_v<>, "mismatching square parenthesis");
        } else if constexpr (error == error_reason::TRIED_TO_ACCESS_OUT_OF_INPUT_RANGE) {
            static_assert(false_v<>, "tried to access out of input range");
        } else if constexpr (error == error_reason::TRIED_TO_ACCESS_OUT_OF_CELLS_RANGE) {
            static_assert(false_v<>, "tried to access out of memory range");
        } else if constexpr (error == error_reason::OUTPUT_CAPACITY_IS_TOO_SMALL) {
            static_assert(false_v<>, "output capacity is too small");
        } else {
            static_assert(false_v<>, "unhandled error!");
        }
    }
}();

C++17ではconstexpr if文を使ってコンパイル時に定数式の結果に応じて処理内容を変えることができます.上に示したコードはインタプリタのコードから output の部分を抜き出してきたものですが,if constexpr (result.index() == 0) で処理を切り分けているのが分かると思います.この例では resultがコンパイル時に計算されてしまっているのでその結果を利用することができ,もし result.index() == 0 が成立したら,すなわち result に出力結果が格納されている場合はバイナリには条件が真のときに実行される処理のみが残され,逆に result.index() == 0 が成立しなかったら,すなわち result にエラーコードが格納されている場合はバイナリには条件が偽のときに実行される処理のみが残されます.一見メリットが分かりにくいかもしれませんが,これを応用することで次のようなコードが実現できたりします.

template<typename T>
constexpr auto func(const T &n) {
    if constexpr (std::is_arithmetic_v<T>) { // Tが算術型かどうか
        return n * 2; // 2倍した値を返す
    } else { // 上と下とで返り値の型が違う!
        return "value is not arithmetic"; // 2倍できない旨を返す
    }
} 

int main()
{
    std::cout << func(42) << std::endl; // 84
    std::cout << func("Hello, world!") << std::endl; // value is not arithmetic
}

output の例では result.index() == 0 でない場合は値を返さずに static_assert でコンパイルエラーにします.result に格納されているエラーコード別にメッセージを変えているというのがなんとなくわかると思います.

実行してみた

作ったインタプリタが実際に動くかどうかを確認してみました.実行したbrainfuckのプログラムは Hello world! を出力する次のコードです.

++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.

結果として次のような出力が得られました.とりあえず問題なく動作している様子が確認できました.

Hello world!

本当にコンパイル時に実行しているのか

動作している様子が確認できているとはいえ,実行されているタイミングがコンパイル時でないことを確認できないことには意味がありません.そこで g++ -S でアセンブリを出力させてみます.以下に main関数に対応する(と思われる)アセンブリを載せます.アセンブリには慣れてないので間違ってたらごめんなさい.

長いので折り畳み

main:
.LFB2057:
    .cfi_startproc
    endbr64
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq %rsp, %rbp
    .cfi_def_cfa_register 6
    subq $64, %rsp
    movq %fs:40, %rax
    movq %rax, -8(%rbp)
    xorl %eax, %eax
    movb $72, -20(%rbp)
    movb $101, -19(%rbp)
    movb $108, -18(%rbp)
    movb $108, -17(%rbp)
    movb $111, -16(%rbp)
    movb $32, -15(%rbp)
    movb $87, -14(%rbp)
    movb $111, -13(%rbp)
    movb $114, -12(%rbp)
    movb $108, -11(%rbp)
    movb $100, -10(%rbp)
    movb $33, -9(%rbp)
    leaq -20(%rbp), %rax
    movq %rax, -48(%rbp)
    movq -48(%rbp), %rax
    movq %rax, %rdi
    call _ZNKSt5arrayIhLm12EE5beginEv
    movq %rax, -56(%rbp)
    movq -48(%rbp), %rax
    movq %rax, %rdi
    call _ZNKSt5arrayIhLm12EE3endEv
    movq %rax, -40(%rbp)
.L3:
    movq -56(%rbp), %rax
    cmpq -40(%rbp), %rax
    je   .L2
    movq -56(%rbp), %rax
    movq %rax, -32(%rbp)
    movq -32(%rbp), %rax
    movzbl   (%rax), %eax
    movsbl   %al, %eax
    movl %eax, %esi
    leaq _ZSt4cout(%rip), %rdi
    call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_c@PLT
    addq $1, -56(%rbp)
    jmp  .L3
.L2:
    movl $0, %eax
    movq -8(%rbp), %rdx
    xorq %fs:40, %rdx
    je   .L5
    call __stack_chk_fail@PLT
.L5:
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

上のアセンブリのうち,以下に示す部分

    movb $72, -20(%rbp)
    movb $101, -19(%rbp)
    movb $108, -18(%rbp)
    movb $108, -17(%rbp)
    movb $111, -16(%rbp)
    movb $32, -15(%rbp)
    movb $87, -14(%rbp)
    movb $111, -13(%rbp)
    movb $114, -12(%rbp)
    movb $108, -11(%rbp)
    movb $100, -10(%rbp)
    movb $33, -9(%rbp)

で Hello world! に対応するバイト列 72 101 108 108 111 32 87 111 114 108 100 33 が即値で埋め込まれているのが確認できます.どうやら本当にコンパイル時に処理ができているようです.

制約

コンパイル時に動作する都合上,このインタプリタはコンパイラの制約を受けます.特にGCCの場合は -fconstexpr-loop-limit と -fconstexpr-ops-limit でループや演算の回数が縛られます.まあこれらの数値は大きくすることができますが,その分だけコンパイルにかかる時間が伸びますし,brainfuckのプログラムが無限にループしている場合は先ほどの数値を大きくしてもコンパイルできません.まあでもお遊び程度でbrainfuckのプログラムを実行させるならほとんど引っかかることはないんじゃないでしょうか.

まとめ

コンパイル時にbrainfuckを実行するインタプリタを作成しました.C++は規格が新しくなるたびにコンパイル時計算ができる処理が増えていっていることもあり,C++17の時点でもかなり楽にコンパイル時計算が書ける印象があります.実際,brainfuckのインタプリタを作る過程でもそこまで悩むことはありませんでした.この記事を読んでコンパイル時計算に興味を持った人(そんな人おるんかな)がいたらまずは副作用のない関数から順次 constexpr をつけていくといいんじゃないですかね.

最後までお読みいただきありがとうございました.

山に登る

蝶ヶ岳山頂からの光景

この記事はあくあたん工房 Advent Calendar 2020の14日目の記事です.遅刻して16日投稿になっていますが一応カレンダー上では2日連続で帰宅志願者がお送りします.何を書くかでとても迷ったのですが,最近大学の実験レポートやら課題やらでヘトヘトになっているところを癒やしを求めて出身高校の山岳部の山行にOBとしてついていったことから僕の趣味の「登山」のことについて書こうと思います.

いきなりですが「登山」というのは奇妙な行為です.交通機関を駆使して目的の山に赴いては,時には辛い思いをしながら延々と山道を何時間も登ったり降りたりして目的地を転々とし,最後に家に帰るわけです.私はどちらかといえば都市部の方に住んでいますから,山に行くだけでも数千円がかかりますし,そこから山に登るわけですから装備を整えたりして更にお金がかかります.誤解を招かないように先に述べておきますが,山に登る行為自体は特段おかしいものではありません.森林資源の獲得だったり都市間移動だったりあるいは宗教的な意味合いがあったりとそういった目的で古くから山に登る人はいます.しかしながらどうでしょう.登山,特に趣味や生き様としての「登山」というのは山に登るという行為自体を目的としています.ますます奇妙に思えてきませんか.どうして「登山」をするのか,人によって理由は多種多様ではありますが,この記事では僕の登山歴を振り返りつつ僕なりの理由付けをしてみようと思います.

僕が本格的に山に登り始めたのは高校一年生の頃です.高校受験を無事終えどの部活に入るかで悩んでいたとき,当時の山岳部公式Twitterアカウントから一枚の写真が流れてきたのをきっかけに山岳部に入部しました.山岳部のアカウントが途中で変わったために残念ながらその写真を探し出すことができなかったのですが,その写真は記憶に間違いがなければ立山で撮影されたもので,吸い込まれそうなほど青く透き通った空を突き刺さんとばかりに山々が堂々と佇んでいる光景が収まっており,思わず見とれてしまったことをよく覚えています.

山岳部は月一回の山行に参加すれば普段の活動に来なくてもOKという体制で運営されており,そんな山岳部のゆるい部分がだらしない僕の性格と適合したのかとても居心地が良い場所でした.登山というのは体力を使うものですから,月一回の山行に参加するだけで今後もついていけるのかという不安もありましたが,これが思いのほか慣れてしまうもので,体力を使うことに変わりはありませんが山に登るときのしんどさというものはどんどん消えていきました.

そうして山岳部として山に登り続けるようになったのですが,僕の中で登山が重要な立ち位置を占めるようになった決定的な出来事といえば一年生のときの山岳部の合宿です.一年生のときの合宿の行き先は上高地で,徳沢キャンプ場を幕営地としていました.合宿二日目となり,待ち望んでいたとにかく標高の高い山1である蝶ヶ岳に向けていよいよ出発したのはいいのですが,片道5kmで標高差1,100m2の山道で最初は似たような景色が延々と続くのでなかなかしんどく感じられました.

蝶ヶ岳に至る登山道.最初は新鮮に感じるがこの景色がずっと続く

絶景を期待していたばかりに似たような木々の風景が続くのに少しウンザリしながら登り続けていると,徐々に植生が低木に置き換わっていきます.

森林限界に至り突然視界が開ける

歩きながら「あの景色がいよいよこの目で見られる」と期待も頂点に達したとき,目の前に待ち望んでいたそれが現れました.

あの景色を僕はどのように言葉で表せばいいのか今でも分かりませんが,鮮明に思い出せるほどに僕の脳裏に焼き付いています.信じられないほど青い空.容赦なく照りつける太陽.透き通った冷涼な大気.四方を埋め尽くす巨大な山々.頭上ではなく眼下を流れる雲海.現地でしか味わうことのできないあの空気感.あの場所を去るのが惜しまれたのでせめて写真を取って記録に残そうと努力しましたが,どうやっても僕が目にしているあの光景を写真に収めることができません.しかしながら時間は残酷にも過ぎていき,興奮も冷めやらぬうちに下山の時間をむかえてしまいました.

合宿はまだ続き,三日目は積雪のために当初予定していたルートを変更して涸沢に向かったのですが,こちらもやはりなんというか...すごかった.どうやって言葉で表すべきなんでしょうか.

涸沢は氷河の侵食作用によって生じたカールという地形をしているのですが,今にも僕を飲み込まんとするような,そういった迫力がありました.二日目の時点で「写真に収めるのは無理だ」という結論に至ったのでこちらは写真を数枚取る程度におさめてあとは場の空気感を楽しむことに専念しました.それでもやはり下山するのは惜しまれましたが...

合宿を終えてからは大会の練習を積んだりとか後輩ができたりとかいくつかの大会で優勝したりとか二年連続でインターハイに出場しているのに僕の代では出場できなかったりとか色々ありましたが,一年生のときの合宿と比較すればまあどれも些末なもの3です.二年生のときの合宿もやはり感動があったのですが,流石にくどいのでここで述べるのはやめておきます.

高校を卒業して大体二年半とちょっとを過ぎますが,まだ僕は日帰りで行ける山を中心に登り続けています.先日はこの記事の冒頭でも述べたように山岳部の通常山行にOBとして参加し,武奈ヶ岳に登りに行きました.山岳部で活動していた中で山に登るときのしんどさというものは消えましたが,やはり未だに山に登るのは疲れますし足が痛くなります.それでも登ることだけを考えて歩き続ければペースが多少遅かろうがいずれは山頂に着きますし家に帰れます.まあ時には引き返す勇気も必要ですが.

合宿のときの記憶は未だに鮮明に残っており,今でもあの日と同じ空気感をもう一度味わいたいと思っています4.僕の家から日帰りで行けるような山では合宿のときのような感動はありませんが,一旦登山のしんどさが消えてしまえば身の回りのことから一旦距離を取って山の中に身をおくだけでもなんだか楽しく感じます.だから僕は山に登り続けるのでしょう.

いつも技術系の記事を書いているのでどうしても気持ち悪く微妙な文章になってしまいました.まあ初めての試みなので大目に見てやってください.友人の言葉を借りてこの記事の締めとしたいと思います.

"なぜ山に登るのか?"は実際に山に登った人だけの特権ですよ


  1. 標高の高い山を意図して「高山」でググったら岐阜県高山市という飛騨山脈付近の市町村が出てきたので地名ではないことを明示するためにわざわざ「標高の高い山」という表現にしました↩

  2. 徳沢キャンプ場から長堀山を経由して蝶ヶ岳に至るルートでの当時の記録による大体の数値.地形図に糸を乗せながら測った記憶があるが正直なところ今となっては自信がない↩

  3. 山岳部では伝統的に毎年春と秋の大会に出場することになっていました.別に大会に出たいわけではなかったのですが,大会では1チームを4人で組まなければいけない一方で僕の代の部員は5人しかおらず,しかも1人が兼部していて大会の練習ができる状況ではなかったので残った4人で仕方なく大会に出ることになったわけです.他の3人も大会出場に否定的だったのになんでいくつかの大会で優勝できたのか今でも分かりません.最後の大会が終わったときは喜びさえしました.まあでも優勝して全校集会で表彰されるのは悪いものではありませんでした↩

  4. 卒業後も僕を含めた同期の5人で毎年夏山の計画を立てるのですが,大学浪人してたり予定日に台風が接近したりCOVID-19が蔓延しだしたりで毎回なかったことになってなかなかうまくいきません↩

C++で型に特定のメンバ関数が宣言されているときだけその関数を呼び出す処理を実装する

お久しぶりです,帰宅志願者です.この記事はあくあたん工房 Advent Calendar 2020の13日目の記事です.なお執筆時点で14日目になっていて大遅刻しております.ごめんなさい.前置きはこれぐらいにして本題へ.

型に特定のメンバ関数が宣言されている場合はその関数を呼び出したい...

と思うことが最近C++を書いていて思うことがありました.これを実現するには

  1. まず型に特定のメンバ関数が存在するかを確認し
  2. 存在しているかどうかで処理を分岐する

ことが必要です.私はC++初心者なので頭をひねってみてもどのようにすれば良いのかがわかりません.というかそもそもこれが実現できるのかさえ怪しく思えます.

ということでGoogle先生に聞いてみた.

型に特定のメンバ関数が存在するかを確認する

SFINAEを使うバージョン

最初に出てきたのがSFINAEを用いたこちらの形.型Tにfunc()というメンバ関数が存在するかどうかを調べています.

template<typename T>
struct has_func {
private:
    template<typename U, typename = std::void_t<decltype(std::declval<U>().func())>>
    static std::true_type check(U);
    static std::false_type check(...);
public:
    static constexpr bool value = decltype(check(std::declval(T)))::value;
};

以下のようにして型に特定の関数が存在するかをコンパイル時にbool値で得ることができます.

has_func<T>::value;

メタ関数を使うバージョン

最初の結果で満足せずにGoogle先生を質問攻めにしたところ std::experimental::is_detected なるものが出てきました.これはまだC++で実験的な扱いを受けているメタ関数ですが,これを使うと次のようにしてメンバ関数の存在を調べることができます.

template<typename T>
using func_t = decltype(std::declval<T>().func());

template<typename T>
using has_func = std::experimental::is_detected<func_t, T>;

こちらも先ほどと同様に以下のようにして特定の関数が存在するかをコンパイル時にbool値で得ることができます.

has_func<T>::value;

SFINAEを用いたバージョンと比較してとてもスッキリとした印象を受け,良さげに思えます.しかしながら, std::experimental::is_detected はまだ実験的なものですから使用が難しいところでしょう.幸いなことに std::experimental::is_detected は数行の実装で実現することができます.そこで以下のように実装してしまうことにしましょう.

namespace detail {
    template<typename AlwaysVoid, template<typename ...> typename Op, typename ...Args>
    struct detector : std::false_type {};

    template<template<typename ...> typename Op, typename ...Args>
    struct detector<std::void_t<Op<Args...>>, Op, Args...> : std::true_type {};
}

template<template<typename ...> typename Op, typename ...Args>
using is_detected = detail::detector<void, Op, Args...>;

あるいは自分で実装しなくてもBoost.TypeTraitsがすでに実装を提供していますから,そちらを使っても良いと思います.

メンバ関数が存在しているかどうかで処理を分岐する

先程まででメンバ関数が存在しているかどうかがbool値で得られることがわかったのでこれを使って実装します.やり方としては愚直にif文を使ったりSFINAEを使う方法がありますが,もう2020年も終わろうとしている時期ですからC++17の恩恵を被ることにしましょう.constexpr if文を使います.

T t;
if constexpr (has_func<T>::value) t.func();

if文と同様の記述量でSFINAEと同様に実体化するかどうかの制御をすることができます.清々しいほどにシンプルですね.

まとめ

C++でここまで込み入ったコードは初めて書きましたが,静的型付け言語であるC++でこんな処理ができるのかと驚くばかりでした.私はC++初心者なので(保険)記事に間違いや気になる点などがありましたら優しく指摘していただけると幸いです.以上「C++で型に特定のメンバ関数が宣言されているときだけその関数を呼び出す処理を実装する」でした.