2018年5月30日水曜日

Visual C++ の wprintf の独自仕様

今でこそかなり改善されましたが、Visual C++ は C/C++ 標準規格に沿わない独自仕様が多いことで有名です。
その中で意外と知られていないと思われるのが、wprintf 系関数で文字列を出力する際の指定子の違いです。

printf で %s を 指定した場合は const char *、%c を指定した場合は char (実際に渡るのは int ですが)を要求されます。

printf("%s %c\n", "string", 'c');

ここまでは当然のことですが、wchar_t が入ってくると話がややこしくなります。
C/C++ の規格上、wprintf などの wchar_t 系の関数であっても、%s は const char *、%c は char が要求されます。
そして、wchar_t を扱いたい場合は、%ls と %lc を使わなければなりません。

/* wprintf に char の文字列と文字を渡す場合 */
wprintf(L"%s %c\n", "string", 'c');
/* wprintf に wchar_t の文字列と文字を渡す場合 */
wprintf(L"%ls %lc\n", L"string", L'c');

しかしながら、Visual C++ はこの規格に従っておらず、wprintf に %s と %c を指定した場合、wchar_t であると解釈します。
また、char を渡したい場合は独自仕様の指定子 %hs と %hc を使う必要があります。

/* Visual C++ の独自仕様により、%s と %c は wchar_t が要求される */
wprintf(L"%s %c\n", L"string", L'c');
/* char を渡す場合、独自仕様の %hs と %hc を使用する */
wprintf(L"%hs %hc\n", "string", 'c');

これは ANSI コードページから Unicode に移行する際にソースコードの変更が少なく済むようにした結果でしょうが、 標準規格に沿って書かれたプログラムが正しく動作しなくなってしまいます。
ただ、Windows 以外の UNIX などで wchar_t が余り使われないことから、実際に問題になる場面が少なかったものと思われます。

しかし、やはり規格に沿っていないのは問題があるということで、Visual C++ 2015 の CTP で標準規格通りの動作になるように変更が行われました。
デフォルトでは規格通りの動作を、_CRT_STDIO_LEGACY_WIDE_SPECIFIERS を定義することで従来の動作になるという仕様です。
C Runtime (CRT) Features, Fixes, and Breaking Changes in Visual Studio 14 CTP1 | Visual C++ Team Blog
これで正しくはなりますが、今更そんな変更をすれば当然既存のコードに大きな影響を与えてしまいます。
結局この変更は取り下げられてしまいました。

その代わりとして、_CRT_STDIO_ISO_WIDE_SPECIFIERS を定義すれば規格準拠の動作になる仕様とされました。
Format Specifiers Checking | Visual C++ Team Blog

/* _CRT_STDIO_ISO_WIDE_SPECIFIERS の定義で Visual C++ でも規格準拠の動作 */
#define _CRT_STDIO_ISO_WIDE_SPECIFIERS
#include <stdio.h>
int main(void)
{
    wprintf(L"%s %c\n", "string", 'c');
    wprintf(L"%ls %lc\n", L"string", L'c');
    return 0;
}

ただ、TCHAR を使って tchar.h の _tprintf などを使う場合、Unicode とそうでない場合とで指定子も変えるようにしないといけなくなりますので、そこは面倒になります。

なお当然ではありますが、_CRT_STDIO_ISO_WIDE_SPECIFIERS を指定したとしても wsprintf などの Win32 API の動作は変わらず、独自仕様のままです。

0 件のコメント:

コメントを投稿