- 非Windows
- Windowsのコンソール出力と標準出力
- 1. 素直に変換してstd::coutする
- 2. UTF-16に変換してWriteConsoleW()する
- 3. 標準出力をユニコードモードにする
- 4. コンソールのコードページを変更してUTF-8バイト列を直接流し込む
- 5. Boost.Nowideを使用する
- UTF-8の直接出力 in Windows
- 絵文字の表示 in Windows
- 検証環境
- 参考文献
- 謝辞
非Windows
おそらくほとんどの場合、非Windows環境ではchar
のエンコードがUTF-8なのでそのまま出力できるはずです。しかし、C++20では標準出力ストリームに対するchar8_t char16_t char32_t
のoperator<<
がdelete
されているため、そのままではコンパイルエラーになります。でもまあchar
がUTF-8なのですから、こう、ちょっとひねってやれば、無事出力できます・・・
#include <iostream> #include <string_view> int main() { using namespace std::string_view_literals; auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv; std::cout << reinterpret_cast<const char*>(u8str.data()) << std::endl; }
問題なのはWindowsさんです・・・
Windowsのコンソール出力と標準出力
C言語ではI/Oをファイルとそれに対するデータのストリームとして抽象化しています。ファイルの読み書きによって動作環境との通信(すなわちI/O)を制御し、規定しています。標準出力(stdout
)や標準入力は(stdin
)は標準によって予め開くファイルが規定されているストリームで、これらにおいてのファイルとはコンソール(端末)です。
C++もCからこれらのことを継承し、多くのI/O関数はCのものを参照しているため、この辺りの標準I/Oストリームに関することは共通しています。
従って、C/C++の範囲から見た標準出力とはとりあえずは何かのファイルに対する出力として考えることができます。特に、標準出力が受け取った文字列をどう表示するかというところはファイル出力の先の話であり、C/C++が感知するところではありません。
標準IOストリームのモード
C言語においてのファイルストリームには3つのモードがあり、ファイルオープンに使う関数種別fopen/wfopen
およびその引数、もしくはそのファイルストリームに対して最初に使用した関数で決定されます。
- テキストモード
- ユニコードモード
- バイナリモード
テキストモードではストリームへの入力データをテキストデータ(マルチバイト文字列 : char
文字列)だとして処理します。改行コードの変換やロケール対応がここで行われます。標準入出力のデフォルトはこのモードです。この時、ワイド文字列版のI/O関数(std::wcout
など)を使用すると、内部でマルチバイト文字列へ変換されたうえでストリームへ入力されます。
ユニコードモードではテキストモード時の入力データをワイド文字列(wchar_t
文字列)として扱います。それ以外はテキストモードと同様ですが、マルチバイト文字列(char
文字列)を処理するI/O関数(std::cout
など)が使用できなくなります。
バイナリモードはその名の通り入力データをバイト列として扱います。ワイド文字、マルチバイト文字版何れの関数でもその入力データをバイト列として扱い、何の変換も行われません。
ここでのマルチバイト文字列/ワイド文字列のエンコードは規定されていません。WindowsではそれぞれANSI/UTF-16になります。
出力だけに注目すると、最終的にファイルにはモードに応じて次のように書きこまれます。
- テキストモード
char
: そのまま書き込みwchar_t
:char
に変換されて書き込み
- ユニコードモード
char
: 使用不可wchar_t
: そのまま書き込み
- バイナリモード
- 何れの場合もそのまま書き込み
コンソールのコードページ
ここからはWindowsのお仕事です。
Windowsにおける標準出力として設定されているファイルの中はコンソール出力へ繋がっています。C/C++のI/O関数としてはこのファイルに対してテキストモードではchar
のエンコード、すなわちWindows環境ごとのANSI(日本語ならShift-JIS)エンコードで文字列を書きこんでおり、ユニコードモードならばUTF-16で文字列を書きこんでいます。
文字列を表示するためには、その環境の言語毎に最適なエンコードを選択して文字列をそれに変換したうえで表示する必要があります。例えば、ANSI文字列と言っても言語設定によってその解釈に使用すべき文字コードは変化します。
Windowsのコンソールにおいてそれを指定しているのがコードページです。日本語環境ならばCP932というコードページがデフォルトであり、その文字コードはShift-JiSが利用されます。
おおよその場合デフォルトのコードページはその言語環境に合わせたANSIを示すコードページになっているはずなので、テキストモードではコードページに合わせた何かをする必要はありません。バイナリモードの場合も、ファイル出力されてきたバイト列をコードページに従ったエンコードで解釈するだけです。
しかし、ユニコードモード時はそのコードページに対応するエンコードへ変換する必要があります。無駄に思われるかもしれませんが、標準ストリームのモードとコードページは別なのです。コードページはOSで指定されているものなので、表示に当たってはそちらが優先されます。
そのため、ここのコードページを変更してやればユニコードモードにおいてはUTF-16を無変換で通すこともできるかもしれません。
スクリーンバッファ
コードページに従ったエンコードに変換された文字列は最後にスクリーンバッファに出力され、そのままフォントレンダラに渡され表示されます。
このスクリーンバッファ1文字辺りはCHAR_INFO構造体1つによって表現されます。定義を見るに、1文字はwchar_t
1つかchar
1つのどちらかです。これはおそらくVSプロジェクト設定にある文字セットの設定によってどちらが使われるか決定されると思われます。
あえて変更しなければ今時はユニコードになっているはずなので、スクリーンバッファの文字コードひいてはコンソール最終出力の文字コードはUTF-16になっています。従って、コードページのエンコードからスクリーンバッファのエンコードへ再び変換され、スクリーンバッファへと出力・表示されることになります。
CHAR_INFO
1つがコンソールスクリーン上の1文字に当たり、それはwchar_t
1つ分なので、コンソール出力ではサロゲートペアや合字をそのまま扱えなさそうなことがうかがえます・・・
1. 素直に変換してstd::cout
する
一番簡便かつ確実な方法は、UTF-8文字列をANSI(Shift-JIS)文字列へ変換してstd::cout
へ出力することです。
std::codecvt
はC++17で非推奨化してしまったので変換にはWinAPIを利用することにしますが、UTF-8 -> Shift-JIS
の変換を実はそのままできません。MultiByteToWideChar
はchar* -> wchar_t*
へ、WideCharToMultiByte
はwchar_t* -> char*
へ変換するので、どうしても型を合わせられないのです・・・
なのでこれらを連続適用して、UTF-8 -> UTF-16 -> Shift-JIS
という2段階変換することになります。
#include <iostream> #include <string_view> #define WIN32_LEAN_AND_MEAN #include <Windows.h> int main() { using namespace std::string_view_literals; auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv; //UTF-8 -> UTF-16 auto length = ::MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()), nullptr, 0); std::wstring temp(length, '\0'); auto res = ::MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()), temp.data(), temp.length()); //UTF-16 -> Shift-JIS length = ::WideCharToMultiByte(CP_ACP, 0, temp.data(), static_cast<int>(temp.length()), nullptr, 0, nullptr, nullptr); std::string result(length, '\0'); res = ::WideCharToMultiByte(CP_ACP, 0, temp.data(), static_cast<int>(temp.length()), result.data(), static_cast<int>(result.length()), nullptr, nullptr); std::cout << result; }
変換の実装を信用すれば、UTF-8 -> UTF-16の変換で文字が落ちることはありませんが、UTF-16 -> Shift-JISの変換では当然Shift-JISでは受けきれないものが出てきます(絵文字とか)。それはWideCharToMultiByte
がシステムデフォルト値(どうやら??
)で埋めてくれます。
後面倒なのでしてませんが、コード内res
で受けてる変換結果が0
だとエラーが起きてるのでケアした方が良いでしょう。
1.2 UTF-16に変換してstd::wcout
する
しかしとはいえ、二段階変換はさすがに気になりますし、途中でバッファ(wstring
)を確保しなければいけないのも少し気になります。むしろ、std::wcout
でUTF-16出力したくなりますよね。しかし、そのままだとなぜかAscii範囲外の文字が出力されません・・・
std::wcout
と言えども出力先はstd::cout
と一緒です。すなわち、wchar_t
を内部でchar
に変換してから出力しています。そしてどうやら、std::wcout
のデフォルトはCロケールになっており、Cロケールでは変換時に非Ascii範囲の文字をスルーしてくれるようです。華麗です・・・
つまりは、明示的にロケールを指定してあげればいいのです。何を指定すればいいのかさっぱりですが、幸いWindowsではstd::locale("")
とするとその環境のシステムデフォルトのロケールが取得できます。これはWindows限定でポータブルで、外国語環境に行っても適切にその環境のデフォルトロケールを取得することができます。後はこれをstd::wcout
にセットしてやればいいのです。
#include <iostream> #include <string_view> #define WIN32_LEAN_AND_MEANv #include <Windows.h> int main() { using namespace std::string_view_literals; auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv; //UTF-8 -> UTF-16 auto length = ::MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()), nullptr, 0); std::wstring result(length, '\0'); auto res = ::MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()), result.data(), static_cast<int>(result.length())); // wcoutにシステムデフォルトのロケールを設定(Cロケールから変更 std::wcout.imbue(std::locale("")); // 出力 std::wcout << result; }
絵文字は消えましたが日本語出力は出来ているように見えます。しかし、これ以降同じプログラム内でstd::wcout
に何か出力しようとしても何も出てきません。
絵文字が消えているまさにそれが問題で、Shift-JISは絵文字を表現できないので絵文字の変換の際に内部でエラーとなってしまい、それ以降fail状態となり何も出てこなくなるのです。これはfail()
によって検出でき、clear()
によって回復できます。
// wcoutにシステムデフォルトのロケールを設定 std::wcout.imbue(std::locale("")); // 出力 std::wcout << result; // fail状態なら状態を復帰する if (std::wcout.fail()) { std::wcout.clear(); }
std::wcout
で出力したとしてもその内部でコードページに従った変換(結局Shift-JISへの変換)が走っているうえに、変換エラーによって出力できなくなるというのはこれはこれでイケてないですね・・・
2. UTF-16に変換してWriteConsoleW()
する
Windowsにおいて、スクリーンバッファに直接出力するためのAPIがWriteConsoleW()
関数です。この関数はUTF-16文字列を受け取り、指定されたコンソールのスクリーンバッファに直接出力します。
#include <iostream> #include <string_view> #define WIN32_LEAN_AND_MEAN #include <Windows.h> int main() { using namespace std::string_view_literals; auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv; //UTF-8 -> UTF-16 auto length = ::MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()), nullptr, 0); std::wstring result(length, '\0'); auto res = ::MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()), result.data(), static_cast<int>(result.length())); // 出力 ::WriteConsoleW(::GetStdHandle(STD_OUTPUT_HANDLE), result.data(), result.length(), nullptr, nullptr); }
WriteConsoleW()
関数は指定されたコンソールのスクリーンバッファに対して指定されたUTF-16文字列を直接書き込む関数です。スクリーンバッファはコンソールの出力そのもので、ここに書き込まれているデータがフォントレンダラによって表示されます。
出力結果をコピペしてみると分かるのですが、絵文字列は表示出来ていないだけでコピペ先が表示できるもの(VSCodeとか)ならばちゃんと表示されます。すなわち、文字コードとしては出力までUTF-16で行われています。絵文字が出ないのはおそらくコンソールの表示部分がサロゲートペアを扱えないのに起因していると思われます。
WriteConsoleW()
関数は名前の通りコンソール出力専用の関数なので、起動したプログラムにコンソールが割り当てられていない場合に失敗します。すなわち、この関数による出力ではリダイレクトができません。
3. 標準出力をユニコードモードにする
冒頭で説明したように、ユニコード出力だけを使うのであれば標準ストリームをユニコードモードにしてしまえばいいでしょう。Windowsでは_setmode()
関数によってストリームのモードを後から変更できます。
#include <iostream> #include <string_view> #define WIN32_LEAN_AND_MEAN #include <Windows.h> int main() { using namespace std::string_view_literals; auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv; //UTF-8 -> UTF-16 auto length = ::MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()), nullptr, 0); std::wstring result(length, '\0'); auto res = ::MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()), result.data(), static_cast<int>(result.length())); // 標準出力をユニコードモードにする ::_setmode(_fileno(stdout), _O_U16TEXT); // 出力 std::wcout << result; }
この方法実は、ユニコードモードといいつつユニコード直接出力出来ているわけではありませんので、コピペしてみると表示できないものは表示できない事が分かるでしょう。内部でUTF-16 -> Shift-JIS -> UTF-16変換が行われています。変換に失敗した文字列はスペースが当てられているのでしょうか。試してませんが、UTF-16コードページに変更すればあるいは・・・
なお、この方法だとstd::cout
が使用できなくなります。出力するとエラー吐いて止まります・・・。他人の書いたライブラリを使っているときなどはログ出力にstd::cout
が使用されている可能性があるので注意が必要です。
4. コンソールのコードページを変更してUTF-8バイト列を直接流し込む
C/C++I/O関数の範囲内においてはバイナリモードで出力しておき、コンソールのコードページをUTF-8に変更してしまえば、スクリーンバッファへの出力時のUTF-16変換一回で済みそうです。これならばUTF-8文字列をなるべく変換させず、文字が落ちることもほぼないはず・・・
#include <iostream> #include <string_view> #define WIN32_LEAN_AND_MEAN #include <Windows.h> int main() { using namespace std::string_view_literals; auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv; // 出力先コンソールのコードページをUTF-8にする ::SetConsoleOutputCP(65001u); // 標準出力をバイナリモードにする ::_setmode(_fileno(stdout), _O_BINARY); // バイナリ列として直接出力 std::cout.write(reinterpret_cast<const char*>(u8str.data()), u8str.length()); }
最後の方がダブってるのはなんでしょうか、3バイト以上の文字が悪さをしているのでしょうか・・・
この方法でも、VSCodeなどにコピペしてみれば絵文字が正しく表示されるので意図通りになっているようです。また、Ascii範囲内の文字ならばstd::cout
は依然として使用可能ですが、std::wcout
は文字化けします。
コードページを変更してあるので、コンソールはまず入ってきたバイト列をUTF-8文字列として解釈します。UTF-8はAscii文字と下位互換性があるのでstd::cout
はAscii範囲内に限って使用可能となります。しかし、std::wcout
は通常wchar_t
(UTF-16)を受け付けますが、バイナリモードでは無変換でコンソール入力へ到達し、そこでのコードページに従った解釈の際、UTF-16文字列をUTF-8文字列だと思って処理してしまうため、文字化けします・・・
ただし、コンソールのスクリーンバッファへの出力は通常UTF-16なので、UTF-8がそのまま出力されているわけではなく、スクリーンバッファへの出力にあたってはUTF-8 -> UTF-16の変換が行われます。
5. Boost.Nowideを使用する
boost1.73から追加されたBoost.Nowideは<iostream>
や<fstream>
のUTF-8対応をポータブルにするライブラリです。非Windows環境に対してはchar
のエンコードがUTF-8だと仮定しそのまま、Windows環境ではUTF-8 -> UTF-16変換してWriteConsoleW()
などWindowsのユニコード対応APIで出力します。
残念ながらchar8_t
対応はされていない(おそらく厳しい)のですが、これを利用すれば一番最初に紹介した方法がポータブルになります。
#include <string_view> #include <boost/nowide/iostream.hpp> int main() { using namespace std::string_view_literals; auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv; boost::nowide::cout << reinterpret_cast<const char*>(u8str.data()) << std::endl; }
試していないので出力がどうなるのかは分かりませんが、実装を見るにおそらくWriteConsoleW()
を使用したときと同様になるかと思われます。
UTF-8の直接出力 in Windows
無理です。
絵文字の表示 in Windows
通常のコンソールでは無理ですが、Windows Terminalを使えば表示できます。
上記2の(WriteConsoleW()
による)方法での出力 in Windows Terminal
上記4の(コードページ変更とバイナリモードによる)方法での出力 in Windows Terminal
まだ合字が表示できないみたいですが、今後に期待ですね。
検証環境
参考文献
- コンソール出力関連
- 標準ストリーム - Wikipedia
- ファイルとストリーム - Microsoft Docs
- C言語のワイド文字入出力 - 雑記帳
- C言語のワイド文字入出力 — MSVCRTの場合 - 雑記帳
- C言語のワイド文字入出力 — Windows Console 編 - 雑記帳
- WindowsコマンドプロンプトにUnicode表示 - エンジニア徒然草
- 標準入出力とリダイレクト - EternalWindows
- コンソールへの入出力 - EternalWindows
- Unicode Stream I/O in Text and Binary Modes - EternalWindows
- CHAR_INFO structure - Microsoft Docs
- コードページ - Microsoft Docs
- Windows 10までほとんど手が入れられてこなかったWindowsのコンソール機能 - ASCII.jp
- 各方法関連
- その他
謝辞
この記事の6割は以下の方々によるご指摘によって成り立っています。