読者です 読者をやめる 読者になる 読者になる

syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

可変個引数のダークサイド

(これは2010-11-07に書いた故OCNブログの記事を移植したものです)

C/C++ の可変個引数はとても強力です。こいつにお世話にならない人はおそらくほとんど居ないと言っても過言ではありません。とくに printf/scanf 系の文字列入出力関数は、C++ のストリームが使いづらい*1せいで、たとえ C++ のヘビーユーザーであってもずっと使い続けている人も多いことでしょう。かくいう自分もその一人です。C++ のストリームは型安全なのが良いところなんですが、細かい書式を指定しようとすると、記述があまりにも冗長になりすぎます。

ただ、可変個引数は値をパラメータとして渡した時点で、一旦パラメータの型情報が失われてしまうせいで、型安全なプログラムを記述することができないという致命的な欠点があります。具体的にどんなのがやばいかというと、例えば MFC/ATL プログラムでこんな記述をするときでしょうか。

const char* pParam = "Hello, World!";
CString str;
str.Format("%s\n", pParam);

このコード、MBCS(マルチバイト文字列)設定では普通にコンパイルが通るし、正しい実行結果が得られるんですが、UNICODE 設定ではコンパイル エラーになってしまいます。CString が内部で管理しているバッファは、正確には char 型配列でなく TCHAR 型配列であり、また CString::Format() 関数の第一引数の型 LPCTSTR は const char* 型でなく const TCHAR* 型です。それゆえ、UNICODEシンボルが定義されていない場合にはそれぞれ char 型配列、const char* 型ですが、一方 UNICODE シンボルが定義されている場合にはそれぞれ wchar_t 型配列、const wchar_t* 型になるからです。

UNICODE 定義有無と各種データ型の変化

UNICODE シンボル未定義 UNICODE シンボル定義済み
TCHAR char wchar_t
LPTSTR char* wchar_t*
LPCTSTR const char* const wchar_t*
CString CStringA CStringW
_T("hoge"), TEXT("hoge") "hoge" L"hoge"


しかたねーな、じゃあこれでどうよ?

str.Format(_T("%s\n"), pParam);

確かにこれだと MBCS だろうが UNICODE だろうがコンパイルは通ります。_T() マクロは UNICODE 設定のときにリテラル文字列をワイド文字列化してくれる仕組みになっているからです。しかし pParam の型は相変わらず const char* なので、UNICODE では正しくデータを解釈できず、実行時に文字化けしてしまいます。要するに、「コンパイラが検出できない(コンパイル時には分からない)コーディングミスによるバグ」を作り込んでしまうことになります。CString::Format() 関数は、%s 書式に対応する文字ポインタ型として、const TCHAR* を想定しているからです。つまり正しくは、上記修正に加えてさらに、

const TCHAR* pParam = _T("Hello, World!");

あるいは

str.Format(_T("%s\n"), static_cast<LPCTSTR>(CString(pParam)));

と修正することも必要になります(後者はリテラル以外にも対応できますが、実行時に const char* から CString へ変換するためのオーバーヘッドが加わるので注意)。


ここで何が問題かというと、CString のバッファが char 型であることを想定した既存の古いコード(特に最初 VC++ 7.1 以前の処理系向けに書かれていたがために、VC++ 8.0 以降の新しいコンパイラを使いながらも未だに MBCS バージョンのライブラリを使っているコード)を Unicode 移行するためにコードを修正するときに、コンパイラがエラーを出力してくれている箇所以外にも目を光らせないといけない、ということ。

ちなみに MFC/ATL の CString::Format() の他にも、Win32 API で言うと wsprintf() 関数が可変個引数を用いています。wsprintf() は UNICODE シンボル定義の有無で wsprintfA() もしくは wsprintfW() のいずれかに展開されます。標準Cライブラリの swprintf() とは名前が似てますが出自も機能も別物の関数なので注意。なお、wsprintf() は sprintf_s(), swprintf_s() と比較して、いろいろと制限や問題を抱えているので、よほどのことがない限り使わないほうがいいでしょう。

コンパイル時にはエラーとならず、実行するまでエラーが発覚しない、というのは最も厄介です。なぜなら全てのフローをテストしないとバグが発覚しないということの裏返しでもあるから。テストを自動化しているコードならばともかく、Unicode 関連の修正の影響は自動化の難しい UI まわりに如実に表れるので、余計にタチが悪いです。さらに、書式のミスによりランタイムエラーや例外が送出されるならばともかく、実際は目に見える形で即エラーになることはありません。せいぜい表示がおかしいとか、出力結果にゴミが混ざるとかいった程度で、いったい何が原因で引き起こされた不具合なのかをすぐに特定するのは難しいでしょう(場合によってはホワイトボックステストが必要になります)。単体テストをきっちりやらずに、いきなり結合してビッグバンテスト、なんてプロジェクトだとなおさらです。

WinAPI/MFC/ATL 初心者は、まず間違いなく TCHAR 関連の話が分かっていないというかそもそもワイド文字とか MBCS とか Unicode とか何それ食えんの状態であること間違いなしなので、このことを教えないで初心者に大量の Windows アプリケーションコードを組ませるべきではありません。特に素人に printf 系の書式と可変個引数を使わせると、後で修正&デバッグするのに多大な労力が必要となります。まぁそもそも初心者に十分な教育を施す前に、いきなり製品用プログラムのソースを触らせる時点で間違っているわけですが……

もしあなたが新人を指導する立場にあり、さらにあなたが関わっているソフトウェア製品が(日本語を含む)多言語環境向けに開発されているのであれば、文字および文字列に関する(正しい)知識は最初から徹底的にたたき込むことを強く推奨します。将来的に使い物にならないゴミコードが大量生産されていく前に……


なお、C/C++ は多バイト文字列どころか、文字および文字列自体のサポートがあまりに弱いので、昔からこのあたりは苦労していたところですが、C++11/C11 (C++0x/C1x) ではようやく UTF-16UTF-32 専用の文字型が用意されることになります。個人的には、int8_t / uint8_t 型を組み込み型に昇格させて、(char 型に依存しない形で)言語標準レベルで1バイト整数型をサポートして欲しいんですが。これは int32_t / uint32_t 型にも言えます。でないとオーバーロードがらみの問題が発生して見苦しい上にややこしいです。sizeof(wchar_t) とか sizeof(int) とか sizeof(long long) とかあまつさえ sizeof(char) までもが処理系依存とかいう C/C++ のヘボ仕様は、もういい加減勘弁してほしいです……JavaC# がはるか昔に(というか最初から)サポートしているものを、いつまでたってもサポートしようとしない C/C++ にはホント嫌気が差してきます……

余談:%s 書式と %S 書式

MSVC 付属の CRT およびそれらを内部的に使用している MFC/ATL では、(ISO 標準ではないんですが)%s 書式のほかに %S 書式もサポートしています。こいつを使うと、マルチバイト書式文字列でワイド文字列を可変個引数に受け付けたり、逆にワイド書式文字列でマルチバイト文字列を受け付けたりできます。具体的には、

CStringA strA;
strA.Format("%S\n", L"hoge");

とか、

CStringW strW;
strW.Format(L"%S\n", "hoge");

とかいうことができるようになります。ただし、CString の場合、const TCHAR* 書式文字列において %S を使うと、MBCS 設定と UNICODE 設定では意味が逆になり混乱するので、使用は推奨されません。また、内部的に char ⇔ whcar_t の変換が入るので、統一できるときはしておかないと余計なオーバーヘッドが発生する羽目になります。どちらにせよ、やはり可変個引数は MBCS と UNICODE をきちんと理解している人間のみが使うべきでしょう。

なお、size_t と ptrdiff_t に関しても、サイズがターゲット プラットフォームに依存するので、こいつらを文字列変換する場合にも細心の注意が必要となります。

*1:Boost.Format を使えばかなり改善されるんですが、C++11 においても標準ライブラリには取り込まれていないマイナーライブラリなのが欠点です。また、string/wstring の違いはやはりプログラマーが意識する必要があります。