🐥

C/C++の__DATE__と__TIME__マクロ定数を使う事の是非

2020/11/20に公開

はじめに

古の時代より存在する事前定義済みマクロ定数、__DATE__と__TIME__について、その使用の是非を考えます。
__DATE__はコンパイル時(正確にはプリプロセス時)の日付を文字列化した値で、__TIME__はコンパイル時の時刻(秒単位)を文字列化した値です。大抵の場合、OSのタイムゾーンと時刻に依存した値になるようです。

結論

__DATE__と__TIME__はそれを使わなければ絶対に解決できない課題を解決するためにのみ使い、それ以外の場合に安易に使うべきではありません。特に、バージョン値の代用としての使用は不適切です。GCCも警告を出しますし。

__DATE__と__TIME__マクロ定数を使う目的

最終的なオブジェクトファイルが、どの時点のソースファイルから生成されたのかを知る目的で__DATE__と__TIME__マクロを使う場合があるようです。何故でしょうか?

エンドユーザーに対して実行ファイルやバイナリのライブラリファイルのみを提供し、ソースファイルを提供しない場合に、提供したそれらソフトウェアに不具合が発覚したとします。
 この場合、不具合を引き当てたエンドユーザーが使っているバイナリファイルから、それらのビルドに使ったソースファイルのバージョンを特定しなければ、ソフトウェア提供側で不具合を再現し、修正することはできません。

提供したバイナリファイル群に代表バージョン値を割り当てておき、それらのビルドに使ったソースファイル一式を保存しておくという方法もあります。しかし、QAプロセスが確立されていない組織では、対応するソースファイル一式の組み合わせ情報が失われている、という事が往々にしてあります。(驚くべきことに、開発者一個人の裁量でローカル環境でビルドし、リリースしてしまうという事もあります。)
 時には、代表バージョン値が同一ではあるものの、バイナリ不一致になるバイナリファイル群が2つ以上、異なるエンドユーザーに提供されていた、ということすらあります。要は代表バージョン値の更新忘れですね。(SCMサーバへ上げ忘れている場合もあり、完全に復元不可能な場合はゲームオーバーです。SCMサーバ上のリビジョンのソースファイルを使ってバイナリファイルを提供し直しましょう。)
 代表バージョン値を信頼できないとなると、不具合が発生したバイナリファイル群を再現できるソースファイルの組み合わせを、ビルドしながら地道に探すしかありません。地獄の始まりです。

こうなってしまった時のために、ソースファイルに変更を加える度にインクリメントするソースコードバージョン値を埋め込んでおけば、ソースファイルの組み合わせ探索範囲を減らせるかもしれません。
 しかし、ソースファイルが大量に存在するのであれば、代表バージョン値の更新忘れのように、ソースファイル単位でのバージョン値も更新忘れが発生します。全てのソースファイルのバージョン値が正しく更新されている確率は、ソースファイルが増えれば増えるほど低くなっていきます。また、C/C++は#includeで指定されたファイル(いわゆるヘッダファイル)を結合してコンパイルするため、ヘッダファイルの変更が、それをインクルードしているソースファイルのビルド結果を変えてしまう場合もあり、変更の追跡には一定の技術力と労力が必要です。

そこで誰かが「いいアイディアがある」と言い出して、__DATE__マクロと__TIME__マクロでコンパイル時刻を埋め込み始めるのです。「これならビルドする度に固有の値を埋め込めるので、バージョン値の代わりになるし更新を忘れることもない。」

何が問題なのか

本質的な問題は、ソースファイルリビジョンと、ビルド日時は直接的には無関係ということです。過去のリビジョンのソースファイルを使って、今日ビルドするかもしれません。そうなると、ビルド日時は、それをビルドするために使ったソースファイルのリビジョンを特定する目的には使えません。

さらに、ビルド日時を埋め込むようすることで、全く同じリビジョンのソースファイル一式を使ってビルドしても、同じバイナリファイルを生成できなくなってしまいます。ビルド日付を埋め込むことで、ビルドプロセスに再現性がなくなります。

gccの警告オプション「-Wdate-time」を使用すると、__DATE__または__TIME__を使用している場合、「再現性のないビルド」である事を警告してくれます。

警告を確認するためのコード。
https://wandbox.org/permlink/0qxHDXwHjKcahjVF

C/C++は最終的に実行可能バイナリファイルを出力するコンパイル言語なので、ソフトウェアテストの対象はソースファイルではなく、コンパイルによって生成された実行可能バイナリファイルです。そのバイナリファイルの生成に再現性がなくなるということは、同じソースファイルからビルドしたバイナリファイルであっても、それらは複数のバージョンが存在し、それぞれにテストが必要だという事になります。
 そのため、過去にリリースしたバージョンを後々リリースしたいのであれば、二度と再現できないものとしてバイナリファイルを残しておくか、同じソースファイルリビジョンでバイナリファイルを作り直した場合は、過去に行ったテストをもう一度実行する必要があります。

おわりに

ビルド日付を埋め込むことは、結局のところ、バイナリファイルの元になったソースファイルの特定にほとんど役に立たないだけでなく、そもそもバイナリファイルとソースファイルの一対一関係も崩してしまい、ソフトウェアのリリースプロセスに対して取り返しのつかない運用コストを上乗せしてしまう可能性があります。

ソフトウェア規模が小さく、開発関係者も少なく、過去にリリースしたバージョンのサポートがほぼ不要であれば、ビルド日付の埋め込みはバージョン値の埋め込みよりも低コストでうまく機能するかもしれません。しかし、いずれの日か、それこそ、そのソフトウェアがうまくいけばいくほど、やがて足枷になっていく事でしょう。

バージョン追跡の仕組みとして、__DATE__と__TIME__マクロでビルド日付を埋め込む事はやめておきましょう。

Discussion