この記事はC++アドベントカレンダー2021の記事です。
小ネタですが今日の分が埋まっていなかったので。
std::source_location
とは
std::source_location
は、その名の通りソースコード中の位置を表す情報が入った構造体です。C++20以降で使うことができ、<source_location>
で定義されています。
https://cpprefjp.github.io/reference/source_location/source_location.html
__FILE__
や__LINE__
のモダンなバージョンだ、という認識でだいたいOKです。
この構造体は普通、std::source_location::current()
で作ります。この関数は呼ばれた位置を示すstd::source_location
を返します。デフォルトコンストラクタもありますが、コンテナに入れるときに気を使わなくていいくらいの用途しかないと思います。
明示的にstd::source_location::current()
を呼ぶのは書くのも意識するのも面倒なので、ほとんどの場合において、これは関数のデフォルト引数として使います。この機能のいいところは、それでも意図した通りに動くところです。
void f(std::source_location loc = std::source_location::current()) { std::cout << "in function " << loc.function_name() << " at " << loc.file_name() << ":" << loc.line() << ":" << loc.column() << std::endl; } f(/*この中でcurrent()が呼ばれるので、この位置が出力される*/);
ログ取りに便利ですね。
この構造体はコピー構築・コピー代入ができるので、持ちまわることもできます。たとえば
class X { public: X(std::source_location loc = std::source_location::current()) : loc_(loc) {} private: std::source_location loc_; };
のようにしておくと、Xを構築した箇所の情報を追跡することができます。
問題
上記のようにsource_location
はデフォルト引数として使われることが多い、というか、あまり手動で構築する機会はありません。ですが、デフォルト引数が使えない場所というのはあります。可変引数テンプレートです。
まず、デフォルト引数は省略される可能性があるため、前にも後ろにもあるとどの引数がどれに対応するのかあいまいになります。なので省略不可能な引数は全て、省略可能なものの前に定義しなければならないと決まっています。
int f(int a = 10, int b, int c = 30); // error! f(10, 20); // a = 10 ? b = 10 ?
可変引数テンプレートは、任意個の引数にマッチすることができるため、始まって以降全ての引数を吸い尽くします。つまり、可変引数テンプレートの後に何かを明示的に渡すことはできません。1
template<typename ... T> int f(T&& ... args, double x); // error!
よって、デフォルト引数はそれ以外の引数よりも後ろに置かなければならないものの、可変引数テンプレートはそれ以外の引数よりも後ろに置かなければならない、というわけでこの二つは競合します。
template<typename ... T> int f(T&& ... args, // error! std::source_location loc = std::source_location::current());
ですが、特にログなどの用途では可変引数テンプレートは便利です。なんとかこれらを同時に使えないものでしょうか。
解決方法
今回の問題は、可変引数テンプレートとデフォルト引数が同じレイヤーに登場していることに起因しています。方針としては、この二つが登場するレイヤーを分ければよいということになります。
そこで、「関数のように見えるが実は違うもの」として自然にクラスを思い浮かべます。ですが、クラスのoperator()
だと、クラスの初期化が終わっていなければなりません。クラスの初期化が終わっているということは、クラステンプレートは実体化されているということです。これだとコンストラクタ呼び出しのために余計な括弧が必要ですし、可変引数テンプレートもコンストラクタ呼び出し時に明示的に与えなければなりません。
なら、コンストラクタ呼び出しを使うのはどうでしょう。これならとりあえず第一の問題、コンストラクタのための余計な括弧が必要になることは解決し、見た目上は普通の関数にしか見えなくなります。
さらに、クラスのテンプレート引数に可変引数テンプレートを押しやっておけば、コンストラクタ内では引数のリストが既知になります。するとコンストラクタでデフォルト引数を使えるようになり、可変引数テンプレートとデフォルト引数が競合することはなくなります。
template<typename ... Args> class f { // ここではすでにArgs...は既知(f<Args...>のコンストラクタだから) f(Args ... args, std::source_location loc = std::source_location::current()); // OK! };
これを関数であるかのように書けるよう、(自明な)推論補助を書きます。コンストラクタがクラスのテンプレートパラメータ型の値を直接受け取っているのでなくてもいけるやろと思ったら無理でした。
template<typename ... Args> f(Args...) -> f<Args...>
ここではデフォルト引数は書きません。2
このようにすると、以下のコードが通ります。
f(42, 3.14, "foobar"s);
これは、あたかも関数呼び出しに見えますが、実際にはf<int, double, std::string>
のコンストラクタを呼んで、できたf
のインスタンスを即捨てるコードです。この時点で、コンストラクタに副作用を持たせることによって副作用のあるvoid
を返す関数は実現できます。
では値を返せるようにしましょう。受け取ったものから必要なものを計算して、メンバとして持ちます。どうせf
がクラスであることを知っているのは我々だけなので、暗黙の型変換ができるようにしましょう。
template<typename ... Args> class f { f(Args ... args, std::source_location loc = std::source_location::current()) : result(do_something(args...)) { std::cout << "at " << loc.line() << ":" << loc.column() << std::endl; } operator int() const noexcept {return this->result;} private: int result; }; template<typename ... Args> f(Args...) -> f<Args...> const int a = f(42, 3.14, "foobar"s);
というわけで、可変引数テンプレート関数でstd::source_location
を使っているように見せかけるコードでした。もし返り値をauto
で受けられるとこのf
がそのまま受け取られるのでバレないように祈る必要が出てきます。あとコピーが重いものは適切にmoveしたりする必要があります。
また、代入時の変換は継承でも実現可能です。もしテンプレート周りでややこしくなっていたら試してみるのも手でしょう。
書く前に10秒だけ調べたら新規性がなかったようですが( https://stackoverflow.com/questions/57547273/how-to-use-source-location-in-a-variadic-template-function )、深く考えないことにしました。
ほかにいくつか試しましたが、これはというものは考え付きませんでした3。なんかもっといいアイデアはないもんでしょうか。
限定付きのよりよい解決策
例えば、関数の第一引数が確定している場合は、その第一引数を適当にラップすることで実装可能です。例えば、ログ用の関数でlog_level
を取ることにしましょう。
enum class log_level {debug, info, warn, error, fatal}; // invalid template<typename ... Args> void log(log_level, Args... args, std::source_location loc = std::source_location::current())
log_level
とsource_location
を持つ適当な構造体を作って、そのコンストラクタでcurrent()
を取ることにします。
struct log_level_with_location { log_level_with_location(log_level l, std::source_location loc = std::source_location::current()) : lv(l), loc(loc) {} log_level lv; std::source_location loc; }; template<typename ... Args> void log(log_level_with_location lv, Args... args);
こうすれば、log(log_level::info, args...)
の呼び出しの際にlog_level
からlog_level_with_location
への変換が発生して、log_level_with_location
のコンストラクタでsource_location::current()
が呼ばれます。このコンストラクタはlog(lv, args...)
呼び出し時に呼ばれるので、log(lv, args...)
時点での位置が取得できます。
もし第一引数が決まっているのなら、これがいいですね。
-
一応、メタプログラミングによって最後(からN番目)の引数だけを取り出して何かをすることは可能です(参考→ https://in-neuro.hatenablog.com/entry/2020/05/24/171047 )。このようにすれば、「最後の要素に特定のものを渡しているかどうかチェック」して「最後の要素を特別扱いする」ことが可能ですが、今回はデフォルト引数を渡したいので、これではうまくいきません。関数のdeclarationの時点でデフォルト引数を書く方法が必要です。↩
-
昔@onihusube9さんに教えていただきました → https://twitter.com/onihusube9/status/1447973137916710917?s=20↩
-
思いつくままに検討した方法ですが、lambdaの初期化captureだとlambdaの宣言箇所に、非型template引数ではtemplateの箇所になってしまうので、うまくいきません(templateは実体化された箇所になってくれないかと期待していましたが……)。すべての可変長引数
T
に対してstd::source_location
とT
を持つ構造体value_with_location<T>
を考えて、それをT
から作るコンストラクタでstd::source_location::current()
を呼ぶというごり押しは通りそうですが、value_with_location<T>
のT
の推論がうまくいきません。だからといって型消去をすると型を戻せなくなり、詰みます。↩