google-glogに潜むトリックを解明する
google-glogは非常に有名なロギングライブラリであり、その名前からわかる通りgoogleの人々によって開発されている。使い方は簡単で、
LOG(INFO) << "this is not a drill";
みたいな感じで、LOG()が返すオブジェクトoperator<<で記録したいオブジェクトをつなげていくだけで使える。とても便利である。実は、この便利さの裏には、実はいくつかのトリックが隠れている。適当に見た目を真似して作るだけでは、glogと同じような便利さは実現できないのである。今日は、その便利さを実現しているトリックを紹介したい。
なぜLOG()はマクロなのか
まず、このLOG(INFO)というのは一見、関数もしくはクラスのコンストラクタかなにかに見える。しかしその実体は実はマクロで、以下のように展開される。
LogMessage(INFO).stream()
LOGというクラスがあるわけではなく、あるのはLogMessage型のクラスと、streamメソッドが返すLogStream型のクラスの2つである。それぞれの役割を簡単に説明しよう。まず、LogMessageクラスであるが、実はこいつは大した役目をもっていない。単にLogStreamクラスのメンバを持ち、それをstreamメソッドで返しているだけである。
実際に役に立つ処理を行なっているのはLogStreamクラスの方である。これはバッファ的なオブジェクトで、operator<<で与えられたメッセージをバッファに溜め込み、デストラクタでログ用のstreamに一気に書き出す。LogStream型オブジェクトがなぜデストラクタで書き出しを行うのかは後で説明するとして、まずはなぜこのような分割構成になっているのか、その何故に迫っていこう。
この分割構成だと、LogMessageクラスは大したことをしていなくて、一見、LogStreamクラスだけがあればそれでいいように見えるし、そうなると、LOGというマクロは別に必要なくて、LogStreamクラスをLOGというクラス名で定義すればいいように思われる。しかし、実はそのように作ってみると、うまくいかない。operator<<はそのような使い方を許していないのだ。
LOGをクラスにした場合、LOG(INFO)は一時オブジェクトを返すことになる。そうすると、LOG(INFO) << "a"; みたいな呼出は、operator<<(LOG &log, const char *s) みたいなシグネチャになるのだが、ここで第一引数のLOG &logにconstがついていないのがまずい。一時オブジェクトは関数の引数に渡すときには、値で渡すか、もしくはconst参照にしか渡せないのである。
一時オブジェクトがconst参照にしか渡せないのはなぜか、まだ未だに理由がよくわからないというか、得心がいかないが、どうもそういうものらしい。http://q.hatena.ne.jp/1167111891 とかにもそのような話題がある。
さて、ここまで書くと、glogがどういうトリックで問題を解決しているのか、見えてきたものと思われる。LogMessageクラスとLogStreamクラスがわかれているのは、LogMessageクラスが作った一時オブジェクトはconst参照にしか渡せないのだが、そのメソッドの戻り値には、特にそういった制限はついていないからである。つまり、LogMessageクラスが存在する理由は、単純にconst参照の制限を回避するためである。
このような性質のことを、C++のconstは推移的ではない、と表現することがある。このあたりはD言語の解説の方が詳しい。→Here A Const, There A Const
constオブジェクトのメンバが特にconstではない、という性質を利用するのは、読みにくいコードを生む原因となり得るのであまり望ましい事ではない。今回は他にやり方がないので仕方なくこういう手段を取っていると考えておくべきで、あまり自分の書くコードで濫用してはいけない類の性質である事は、一緒に覚えておくべきだろう。
なぜデストラクタでログを書き出すのか
先程のコードの例をもう一度だそう。
LOG(INFO) << "this is not a drill";
上のコードは、下のように展開される。
LogMessage(INFO).stream() << "this is not a drill";
実際にはLogMessage_INFO.stream()みたいになるのだが、それは今はあまり重要ではないので置いておく。
streamメソッドが返すのはLogStream型のオブジェクトであり、このオブジェクトはoperator<<で受け取った値をバッファに溜め込んで、デストラクタで一気にファイルへと書き出す。
なぜこのような仕様になっているのかというと、同じファイルへ複数スレッドから書き出しを行う際に、ログの行が混ざってしまうことを防ぐためである。
LOG(INFO) << "A" << "B";
みたいな行を複数のスレッドで同時に実行した場合、"AABB"みたいにログが書き出されてしまうことを防ぐためには、あるログ行を構成する文字列が確定してから一気に書き出しを行うしかない。あるログ行でどういう文字列が書き出されるのかが決定した段階で呼び出されるコードがデストラクタしか残っていないため、こうするしかないのである。
これも上のトリックと同じで、本来はあまり使うべきではないやり方である。デストラクタでは失敗する可能性のある操作はできるだけ避けるべきだ。デストラクタでは例外を投げるべきではないので、失敗したとしても、失敗したことを通知する方法がないからである。
- デストラクタの直前で呼ばれる
- 例外によるスタック巻戻しの際には呼ばれない
ようなメソッドをオブジェクトに登録できる機能があれば、このような機能をより適切な形で実現できそうだが、現在のC++の仕様ではデストラクタでファイルへの書き出しを行うしかない。
まとめ
glogでは、
- C++では一時オブジェクトがconst参照にしか渡せないので、LogMessageとLogStreamの2つにクラスを分割する必要がある
- マルチスレッド対応のために、デストラクタでログを書き込むようになっている
実際のgoogle-glogは実はもうちょっとマクロにまみれており、より複雑になっているのだが、だいたい以上のようなことを理解すれば、google-glogのコードを読むこともできるはずである。glogのコードを読んだのは1ヶ月以上前の事で、今は一応ちょこちょこソースコードは参照しながらエントリを書いているけど、厳密に確認しながら書き進めてきたわけではないので、もしかしたら間違いを含んでいるかもしれない。そういったことを了解した上で、ぜひ実際のglogのコードに触れていただきたい。
もっとも、glogには単純なロガーとしての役割だけでなく、segmentation faultで落ちた際にバックトレースをログに吐くという機能があり、そちらの方を読むためには、より高度な知識が必要となる。その辺はまた今度、また元気が出たときに調べてみようと思う。
おまけ:デストラクタで例外を投げてはいけないのか?
google-glogはデストラクタで例外を投げてはいないと思うが、このようなロガーを作るとデストラクタで例外を投げたくなるかもしれない。
でも、デストラクタでは例外を投げてはいけないと教わるし、どうしたらいいものだろうか。
そもそも、なぜデストラクタで例外を投げてはいけないのかというと、例外が発生してスタック巻戻しによってデストラクタが呼ばれた際にそこでさらにデストラクタが例外を投げたら、どこかに開放されない資源が残ってしまう可能性が高いからである。
ということは、
のどちらかの条件を満たせるのであれば、デストラクタから例外を投げても構わないわけだ。
前者に関しては、ポータブルに実装するのはどうも不可能であるらしい、ということが調べた結果わかった。ポータブルでなくても不可能かもしれない。
後者に関しては、保証はできないのだが、実験してみたところ、このロガーのケースではどうも大丈夫そうである。例えば、ロガーに以下のようなコードを渡しても、実行順序的には問題なかった。(テストはg++4.4で行なっている。)ただし、これは、あくまでも1つのコンパイラでたまたまそうなったというだけの話であって、規格書に書いてあるわけではなさそうだ。
class A { public: int i_; A(int i) :i_(i) { cout << "constructor for A" << endl; } }; ostream & operator<< (ostream &os, const A &a) { os << a.i_; return os; } int main(int argc, char *argv[]) { LOG(INFO) << A(3); }
結論としては、なんかデストラクタで例外を投げてもこの場合には大丈夫そうだが、一般的に投げて構わないとは全然言えない、というところだろうか。どうも煮えきらないけど。
特にオチとかはないのだが、C++は難しいですね。