CMake を使うとマルチプラットフォームなプロジェクトを作成することができますが、やっかいなのは文字コードの扱いです。他環境では UTF-8 が使われていますが、Visual Studio では伝統的に Shift-JIS が使われてきました。しかし、近年では改善がみられ、UTF-8 を使ったプログラミング環境が整ってきました。
これらのまとめ記事によると、Visual Studio で UTF-8 を使ったプログラムを使う際には
- コンパイルオプションに
/utf-8
をつける。 - Active Code Page を UTF8 にするための manifest ファイルをプロジェクトに追加する。
- コンソールの出力文字コードを
SetConsoleOutputCP()
で変更する。
ことが有効です。(ただし、コンソール入力は未対応)。
コンパイルオプションの追加
/utf-8
は Visual Studio のメニュー項目からは直接設定できず、その他の追加オプションとして与えます。対して、プロジェクトを CMake から作成する場合は add_compile_option() で設定できます。 ただし、Visual Studio 以外のコンパイラに /utf-8
を渡すとエラーになるので、他環境で使われる可能性を考慮するなら注意が必要です。具体的には if(MSVC) 〜 endif()
で囲むか、Generator expressions を使って
add_compile_options("$<$<CXX_COMPILER_ID:MSVC>:/utf-8>")
と書くと良いです。この例では CXX_COMPILER_ID
が MSVC
でないとき /utf-8
は無効であり無視されます。また、この書き方ではすべての C++ ファイルのコンパイルに /utf-8
が付加されます。特定の実行ファイル(ターゲット)だけに適用したいときには
add_executable(main main.cpp)
target_compile_options(main PRIVATE "$<$<CXX_COMPILER_ID:MSVC>:/utf-8>")
のように記述します。
マニフェストファイルの追加
std::filesystem
などの標準関数や Window の API に UTF-8 の文字列を渡せるようにするには、以下の内容のマニフェストファイル(ファイル名は任意。拡張子は .manifest)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity type="win32" name="..." version="6.0.0.0"/>
<application>
<windowsSettings>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
</windowsSettings>
</application>
</assembly>
を用意して適用することが必要です。Visual Studio から設定するには、プロジェクトの設定でマニフェストツールの入出力から設定しますが、CMake からはソースの一つとして追加するだけで大丈夫です。
add_executable(main main.cpp)
if(MSVC)
target_sources(main PRIVATE utf8api.manifest)
endif()
あるいは
add_executable(main main.cpp $<$<CXX_COMPILER_ID:MSVC>:utf8api.manifest>)
でもOKです。できあがったプロジェクトファイルを Visual Studio から開けば、プロジェクトの入出力マニフェストとして登録されていることが確認できます。これは CMake 3.4 からの対応のようです。
CMake learned to honor *.manifest source files with MSVC tools. Manifest files named as sources of .exe and .dll targets will be merged with linker-generated manifests and embedded in the binary.
コンソールの出力文字コード
本体の main.cpp
には手を加えず、main()
の実行前に SetConsoleOutputCP(CP_UTF8)
を実行する方法として、グローバルオブジェクトのコンストラクタを使う方法があります。
#include <windows.h>
#include <cstdio>
#include <exception>
namespace automatic_utf8 {
struct chcp_utf8 {
static UINT old_;
chcp_utf8() noexcept {
UINT cp = GetConsoleOutputCP();
if (cp != 0 && cp != CP_UTF8) {
old_ = cp;
SetConsoleOutputCP(CP_UTF8);
std::set_terminate(restore);
}
}
~chcp_utf8() noexcept {
restore();
}
static void restore() noexcept {
UINT cp = old_;
if (cp != 0 && cp != CP_UTF8) {
SetConsoleOutputCP(cp);
old_ = 0;
}
}
} auto_chcp;
UINT chcp_utf8::old_ = 0;
}
を用意して
cmake_minimum_required(VERSION 3.15)
set(CMAKE_CXX_STANDARD 17)
project(test_utf8 CXX)
add_executable(main main.cpp)
if(MSVC)
target_compile_options(main PRIVATE "/utf-8")
target_sources(main PRIVATE chcp.cpp utf8api.manifest)
endif()
とすれば、main.cpp
内で windows.h
をインクルードする必要がなく、他環境でも通用するソースプログラムを書くことができます。
main()
終了後にはデストラクタが呼ばれてコードページが復旧されます。exit()
を呼び出して終了した場合も同様です。放置された例外発生によって強制終了した場合にはデストラクタが呼ばれないので、対策として set_terminate()
を設定していますが、ここまでしなくてよいかもしれません。