はじめに
筆者はこれまで長らく Windows 上で開発するときの C コンパイラとしては Microsoft の Visual C++ を愛用してきたが,先日 LLVM の clang-cl の出来栄え(最適化性能のことね)が素晴らしいことを実感した。
- 自炊の画像形式をPNGからWebPに移行する2~WebPユーティリティを評判のClangでビルドする - Qiita
- 自炊の画像形式をPNGからWebPに移行する3~WebPユーティリティを最強Intel C++コンパイラでビルドする - Qiita
ところが別のプログラムで日本語文字列を用いたソースコードを clang-cl でコンパイルしようとすると大量の警告やエラーメッセージで表示されて辟易する羽目に陥った。clang-cl に移行するための最大の障害であろうか。
開発課題
ソースコードを Visual C++ と clang-cl で共用し,かつ同一の環境で実行した場合にいずれも正常に日本語文字列を出力できること。
Visual C++ および clang-cl は Visual Studio 2022 搭載版のものとする。
テスト用プログラム
ファイルの文字コードを Shift-JIS と UTF-8(BOM 付き)の二種類,文字列を char
型と wchar_t
型の二種類で合計四種類用意した。「表」の文字コードは Shift-JIS では 95h, 5Ch と表され,2 バイト目の 5Ch がバックスラッシュ記号 \
に当たるため,Shift-JIS に対応していない処理系だとエスケープ文字と間違えて文字化けを引き起こすはずだ。
#include <stdio.h>
int main() {
puts( "表示" );
return 0;
}
#include <stdio.h>
int main() {
puts( "表示" );
return 0;
}
ワイド文字列を出力する場合には最初に setlocale
を呼び出す必要がある。
#include <stdio.h>
#include <locale.h>
int main() {
setlocale( LC_ALL, "" );
_putws( L"表示" );
return 0;
}
#include <stdio.h>
#include <locale.h>
int main() {
setlocale( LC_ALL, "" );
_putws( L"表示" );
return 0;
}
Microsoft Visual C++ によるコンパイルと実行
Visual C++ では(当たり前だが)四つのプログラム全てが無事にコンパイルでき,いずれも正しく動作する。
LLVM clang-cl によるコンパイルと実行
一方,clang-cl の場合,ファイルの文字コードが Shift-JIS だと警告やエラーが発生する。ファイルの文字コードを UTF-8 にすれば警告は出なくなるが,文字列が char
型だと正常に動作しない。ただし,これはコンソール(コマンドプロンプト)のコードページがデフォルトの 932 の場合である。
結果を表にまとめると以下のようになる。
ファイル名 | ファイルの文字コード | 文字列の型 | コンパイル | 実行 |
---|---|---|---|---|
TEST-CHAR-SJIS | Shift-JIS | char | 警告 | 文字化け |
TEST-CHAR-UTF8 | UTF-8 | char | 成功 | 文字化け |
TEST-WCHAR-SJIS | Shift-JIS | wchar_t | エラー | 実行不可 |
TEST-WCHAR-UTF8 | UTF-8 | wchar_t | 成功 | 正常 |
プログラム TEST-CHAR-UTF8
はコードページを UTF-8 に変更してやれば正常に動作する。
ただし,コードページを UTF-8 に変更すると,今度は Visual C++ でビルドした場合に動かくなってしまうのだ。
別解
Windows 10 バージョン 1803 以降では,ユニバーサル C ランタイムで UTF-8 コード ページの使用がサポートされた。これを利用するためには setlocale
を使用するときに、コード ページとして .utf-8
を指定すればよい。
#include <stdio.h>
#include <locale.h>
int main() {
setlocale( LC_ALL, ".utf-8" );
puts( "表示" );
return 0;
}
この場合,Visual C++ ではコンパイルオプションとして -utf-8
を付ける。オプション -utf-8
は下記の二つのオプションを指定したのと等価である。
- オプション
-source-charset:utf-8
ソース文字セットを UTF-8 にする - オプション
-execution-charset:utf-8
実行文字セットを UTF-8 にする
ファイルに BOM があればソース文字セットが UTF-8 であることは明確なので実行文字セットを UTF-8 にするオプション -execution-charset:utf-8
だけで良いが,単にオプション -utf-8
と指定したほうが短くて楽だからである。
一方,clang-cl のほうは何も指定しなくてよい。デフォルトで -utf-8
が指定されているからだ。むしろ,それを解除できないと考えたほうがよい。
まとめ
結局のところ,ソースファイルの文字コードとコンパイルされた実行ファイルの文字コードは別物なのだ。さらに,コンソールのコードページも意識する必要がある。
A案)既存のコードを流用する場合(あまり修正したくない場合)
-
main()
関数の冒頭でsetlocale( LC_ALL, ".utf-8" )
関数を呼び出す。 - ファイルの文字コードを UTF-8 で保存する。
-
Visual C++ でコンパイルするときはオプション
-utf-8
を付ける。
char
型の文字列に UTF-8 文字列を格納する場合でも,適切なコードページを指定すればユニバーサル C ランタイム関数や,末尾に A がつく Win32 API にそのまま文字列を引き渡すことができるようになったようだ。※Windows10 バージョン 1903 以降
B案)新規に作成する場合(修正を厭わない場合)
- 入出力する文字列を
wchar_t
型で書き直す。 - 入出力関数も
wchar_t
型に対応したものに変更する。 -
main()
関数の冒頭でsetlocale( LC_ALL, "" )
関数を呼び出す。 - ファイルの文字コードを UTF-8 で保存する。できれば BOM を付ける。BOM を付けない場合,Visual C++ でコンパイルするときはオプション
-source-charset:utf-8
を付ける。
C案)新規に作成する場合(英語に不自由しない人)
- 入出力する文字列を全て英文で書き直す。※そもそも日本語を使わなければ良い。
D案)国際化対応を考える人,プロ向け?
- 日本語文字列はリソースファイルとして外出しする。※ワイはやらんけど。
おまけ
A案の場合,もしも新規で作るのなら UTF-8 文字列であることを明示するため C11 で追加された新しい文字列リテラル指定 u8 を使ったほうが良いかもしれない。
#include <stdio.h>
#include <locale.h>
int main() {
setlocale( LC_ALL, ".utf-8" );
puts( u8"表示" );
return 0;
}