背景
- C++11 で
__LINE__
,__FILE__
などを含んでいい感じにログ関数を実装したい - コンパイラの拡張機能使わずにポータブルにやりたい
問題
__FILE__
, __LINE__
__FILE__
や __LINE__
はプリプロセッサで展開されるため, 関数の引数に指定してもうまくいきません.
// does not work as you expected
void log(const char *fname = __FILE__) {
...
}
C++20 では std::source_location
https://en.cppreference.com/w/cpp/utility/source_location がサポートされますのでこの問題は解決しますが, C++20 が普及するまであと数年(現在 2020 年)はかかるでしょう.
C++11 ~ C++17 では結局のところマクロを介する以外に解決方法はありません.
ちなみに C11
から __func__
で関数が取得できるようになりました.
__VA_ARGS__
C マクロでは __VA_ARGS__
で可変引数を対応することができますが, __VA_ARGS__
に割り当たる引数がゼロ個になるとコンパイルエラーになってしまいます.
#define MYLOG(s, ...) mylog(__FILE__, __LINE__, s, __VA_ARGS__)
MYLOG("val = %d", val); // OK
MYLOG("warning"); // NG.
後者は展開すると mylog(__FILE, __LINE__, s, )
となり, コンマのあとにかっこがきて構文エラーになっていまいます.
gcc(clang も?)では, ##__VA_ARGS__
という拡張でゼロ個の引数を対応することができますが, コンパイラ依存になってしまいます.
C++ VA_ARGS のメモ
https://qiita.com/syoyo/items/e5e03a52a953371b82c8
これも C++20 であれば __VA_OPTS__
の導入で解決されます.
解法
C++11 で導入されました variadic template を使います.
ゼロ個の __VA_ARGS__
の問題がありますので, もし __VA_ARGS__
が 0 個になる可能性があるのであれば,
にあるようにクラスを定義しオブジェクトを作るとよさそうです!
#include <cstdio>
#include <cstdlib>
class Message
{
public:
Message(const char *fname, int line) : fname_(fname), line_(line) {}
template<typename... Args>
void write(Args&&... args) {
int nargs = sizeof...(args);
printf("fname %s: line %d, # args %d\n", fname_, line_, nargs);
// fixme: Fully implement `write` function.
}
private:
Message() = delete;
Message(const Message&) = delete;
const char *const fname_;
const int line_;
};
#define msg(...) Message(__FILE__, __LINE__).write(__VA_ARGS__);
int main(int argc, char **argv)
{
msg();
msg("bora");
}
あとは variadic template をぺろぺろ処理すればいけます!
参考
spdlog では普通の引数形式でした. __VA_ARGS__
が必ず 1 個以上ある前提の場合ですね.
pprintpp ではコンパイル時にフォーマット文字列をテンプレートで処理してコンパイル時型チェックしています!
でも ##__VA_ARGS__
を使っているので MSVC では動きません.
また, 完全に型安全というわけでもなく, std::string
など未対応な型の変数を引数に渡すとクラッシュします.