上記の記事で、Shift_JIS(CP932)のコメントにまつわるトラブルを紹介しました。あまり時をおかずして、C/C++でまたハマりました。コメント事件ほどの意外性はないにしろ、事情を知らないと対処しにくいトラブルなので顛末を記しておきます。
この記事の内容は、僕が実際に体験した状況そのものではありませんが、それらしい例題を仕立てたので、ストーリーを追いかけてみてください。
内容:
例題:時刻付きのメッセージキュー
例題として、次のような機能を持つクラスを考えます。
- 文字列メッセージを、時刻(タイムスタンプ)を添えてキューで管理する。
クラスのインターフェースを、次のようなヘッダーファイルに記述します。
// msgq.h #ifndef MSGQ_H #define MSGQ_H #include <queue> #include <string> class MessageQueue { public: void PostMessage(const std::string& msg);// 時刻を付加する const std::string TakeMessage(void); bool IsEmpty(void) const; private: std::queue<std::string> queue_; }; #endif
実装ファイルは次のようです。
// msgq.cpp Version 1 #include <time.h> // time(), localtime() #include <stdio.h> // sprintf() #include "msgq.h" void MessageQueue::PostMessage(const std::string& msg) { time_t now; time(&now); struct tm *ptm = localtime(&now); char timeStamp[30]; // 多め sprintf(timeStamp, "[%4d-%02d-%02d %02d:%02d:%02d] ", ptm->tm_year + 1900, ptm->tm_mon + 1, ptm->tm_mday, ptm->tm_hour, ptm->tm_min, ptm->tm_sec ); queue_.push(std::string(timeStamp) + msg); } const std::string MessageQueue::TakeMessage(void) { const std::string msg = queue_.front(); queue_.pop(); return msg; } bool MessageQueue::IsEmpty(void) const { return queue_.empty(); }
MessageQueueクラスをテストするためのプログラムも書きます。
// test-msgq.cpp #include <iostream> #include "msgq.h" int main() { MessageQueue msgq; msgq.PostMessage("hello"); msgq.PostMessage("good morning"); msgq.PostMessage("good afternoon"); msgq.PostMessage("good evening"); msgq.PostMessage("good night"); while (!msgq.IsEmpty()) { std::cout << msgq.TakeMessage() << std::endl; } return 0; }
このテストプログラムでは、メッセージに付加される時刻がほとんど同じになってしまいますが、それはいいとしましょう。
コンパイル&実行してみます。
$ g++ msgq.cpp test-msgq.cpp $ ./a.exe [2016-05-16 09:14:42] hello [2016-05-16 09:14:42] good morning [2016-05-16 09:14:42] good afternoon [2016-05-16 09:14:42] good evening [2016-05-16 09:14:42] good night
使っているコンパイラはMinGWのgccですが、今回の話では、コンパイラがなんであるかは影響しません。
実装を変えてみると
例題のプログラムはWindows上で動かすとしましょう。はじめに断っておくと、「まーた、Windows特有の話か」と言うと、そうでもなくて、ことの本質は、OS云々というよりはC/C++の仕掛けの問題です。
で、Windows上なので、time.hの関数を使う代わりに、Windows APIのGetLocalTime()関数を使うことにします。GetLocalTime()は次の点でtime.hの関数より便利です。
- time()とlocaltime()の2つを使う必要がなく、GetLocalTime()だけで済む。
- time_t型とstruct tm型(へのポインター)の2つを使う必要がなく、SYSTEMTIME型だけで済む。
- 年月に対して「+1900」「+1」のような補正をする必要がない。
GetLocalTime()版のMessageQueueクラスをVersion 2とします。
// msgq.cpp Version 2 #include <Windows.h> // GetLocalTime() #include <stdio.h> // sprintf() #include "msgq.h" void MessageQueue::PostMessage(const std::string& msg) { SYSTEMTIME now; GetLocalTime(&now); char timeStamp[30]; // 多め sprintf(timeStamp, "[%4d-%02d-%02d %02d:%02d:%02d] ", now.wYear, now.wMonth, now.wDay, now.wHour, now.wMinute, now.wSecond ); queue_.push(std::string(timeStamp) + msg); } const std::string MessageQueue::TakeMessage(void) { const std::string msg = queue_.front(); queue_.pop(); return msg; } bool MessageQueue::IsEmpty(void) const { return queue_.empty(); }
さて、コンパイル&実行。
$ g++ msgq.cpp test-msgq.cpp C:\Users\hiyama\AppData\Local\Temp\ccIGR1jP.o:test-msgq.cpp:(.text+0x55): undefined reference to `MessageQueue::PostMessage(std::string const&)' C:\Users\hiyama\AppData\Local\Temp\ccIGR1jP.o:test-msgq.cpp:(.text+0xa2): undefined reference to `MessageQueue::PostMessage(std::string const&)' C:\Users\hiyama\AppData\Local\Temp\ccIGR1jP.o:test-msgq.cpp:(.text+0xef): undefined reference to `MessageQueue::PostMessage(std::string const&)' C:\Users\hiyama\AppData\Local\Temp\ccIGR1jP.o:test-msgq.cpp:(.text+0x13c): undefined reference to `MessageQueue::PostMessage(std::string const&)' C:\Users\hiyama\AppData\Local\Temp\ccIGR1jP.o:test-msgq.cpp:(.text+0x189): undefined reference to `MessageQueue::PostMessage(std::string const&)' collect2: ld returned 1 exit status
あんれー? エラーです。エラーの内容は、test-msgq.cppから参照されている`MessageQueue::PostMessage(std::string const&)'関数が見つからない、というものです。
なんで、こんなことに
現在時刻の取得に、time()とlocaltime()を使うか、それともGetLocalTime()を使うかは、実装者の判断であって、どっちを選ぶかは自由です。コンパイラに文句を言われる筋合いはないですよね。にもかかわらず、GetLocalTime()を使用するとダメなんですよ。なんで?
実は、(しばらく…が続く)
…
…
…
…
…
…
…
…
GetLocalTime()関数が直接の原因ではありません。問題なのは、MessageQueueクラスのメソッド(メンバー関数)であるPostMessage()です。「PostMessage」という名前そのもの、名前だけが問題なのです。
Windows APIにPostMessage()という関数があり、それと名前がかぶっているのです。しかし、Windows APIのPostMessage()は大域的関数、かたやクラスのメソッドなのだから、同じ名前でも問題がないだろう、と、そう思うでしょ。でも、ダメなんです。影響されちゃうんです。
Windows API関数の名前は、Windows.hのなかでマクロ定義された名前なんです。今回のケースでは、次のマクロ定義が作用します。
- #define PostMessage PostMessageA
.cppソースコード内では、このマクロ定義が働いて「PostMessage → PostMessageA」という名前の置換が起きていたのです。定義された名前が MessageQueue::PostMessageA で、呼び出しに使った名前が MessageQueue::PostMessage ですから、呼び出せるわけがありません。
誰が悪いのか
やっぱりWindowsが悪いんでしょ -- とは言えないです。Windows.hに限らず、意図せぬマクロ置換によってコードを壊されることは、それほど珍しく無いと思います。コンパイルエラー/リンクエラーになってくれればいいですが、たまたま構文的に正しかったりすると相当分かりにくいでしょう。
C/C++では、プリプロセッサが言語処理系全体のなかでけっこう重要な役割を担っています。そのプリプロセッサは、C/C++の構文に関して何も知りません。名前のスコープのことなんか分からないので、闇雲なテキスト置換をやらかします。無知ゆえに間違いを犯しやすいのです。
プリプロセッサって、処理の入り口担当なんですが、処理対象に対する知識を全く持たないヤツに最初の処理を任せるってヒドイと思いませんか。栃木県生まれ東京在住で琵琶湖を見たことがない僕に、滋賀県の観光大使を任せるようなもんです(例えが意味不明? そうですね)。
昔に比べれば、プリプロセッサへの依存度は減ってはいる(inline関数とかconstによる定数とか)でしょうが、危なっかしいプリプロセッサ処理をナシにできないのかなー、と思ったります。