C++でUnicode文字列(UTF-8, UTF-16, UTF-32)を扱うライブラリ
本稿では, C++でバージョンに左右されずに文字を扱うために, 以下の機能を持つライブラリを紹介します.
- 型依存しないUTF-8, UTF-16, UTF-32間の相互変換
- UTF-8, UTF-16文字(コードポイント)ごとのイテレート
- 標準イテレータを使ったイテレート
- 型依存しないイテレータの対応
はじめに
多言語に対応したアプリを開発する際, 多言語に対応した文字コードを使用する必要があります. 多く使用されている文字コードにUnicode規格であるUTF-8があります[1]. 2021年4月でのW3Techsの調査によると, 対象がWebサイトでありますが, Webサイトで用いられる文字エンコーディングで, Unicode規格であるUTF-8が全体の96.7%を占めています[1].
C++では, 文字の扱い方がバージョンごとに様々です. 文字の扱いに関する仕様変更をバージョンごとにまとめたものを以下に示します.
- C++11
- 文字列クラス[2]
- ASCII等の1バイト文字, UTF-8等のマルチバイト文字を扱う
char
型文字列であるstring
- 16ビット長の
wchar_t
型文字列であるwstring
- 16ビット長の
char16_t
型文字列であるu16string
- 32ビット長の
char32_t
型文字列であるu32string
- ASCII等の1バイト文字, UTF-8等のマルチバイト文字を扱う
- 文字列クラス[2]
- C++17
- C++20
本稿では, C++でバージョンに左右されずに文字を扱うために, 以下の機能を持つライブラリを紹介します.
- 型依存しないUTF-8, UTF-16, UTF-32間の相互変換
- UTF-8, UTF-16文字(コードポイント)ごとのイテレート
- 標準イテレータを使ったイテレート
- 型依存しないイテレータの対応
型に依存せずUTF-8, UTF-16, UTF-32間を相互変換することができます. string
型やu8string
型の基になっているbasic_string
型の規格に合った文字列すべてに対応しています. 新しい文字列型が出ても, basic_string
型を基にしている限り新しく変換関数を実装する必要はありません. 変換前と後の文字列型も決まっていないため, 直接望んだ型から型へ変換できます. 例えば, string
型やu8string
型UTF-8文字列をstring
型やu16string
型やwsring
型のUTF-16文字列に直接変換できます.
UTF-8, UTF-16文字(コードポイント)ごとにイテレートできます. 標準の文字列イテレータは, 固定バイトごとしかイテレートできません. 例えば, u8string
型のイテレータは, 1バイトごとにしかイテレートできず, 1~4バイト可変長であるUTF-8文字を一文字ごと正しくイテレートすることができません. 本ライブラリを使うことで, 可変長である文字を正しくイテレートできます.
各コードポイントごとのイテレートに特別なイテレータを使用せず標準イテレータを使用します. 標準イテレータを使用することで, 標準文字クラスが提供する便利関数と連携することができます. 例えば, 標準文字クラスにあるfind
関数である文字を検索し, 先頭のコードユニットを指しているイテレータを取得後, 本ライブラリでそのイテレータから一文字分コードユニットをイテレートするといったことができます.
イテレート関数に渡されるイテレータは, イテレートのサイズに依存し型に依存しません. 例えば, UTF-8文字をイテレートする場合, イテレートサイズが1バイトである, u8string
型, string
型に対応しています. 新しい型が出ても, このサイズが同じである限り, 新しくイテレート関数を実装する必要はありません.
導入方法
以下のフォルダをダウンロードし, プロジェクトに含めてください.
unicode
ライブラリのみインストールしたい場合は, 以下のファイルとそれに付随するファイル(インクルードしているファイル)を, プロジェクトに含めてください.
本ライブラリは, nodecプロジェクト([Welcome!/nodec framework(プラットフォーム開発のためのフレームワーク)])に導入され, メンテナンスは, このプロジェクト下で行われることになりました.
使い方
UTF-8, UTF-16, UTF-32ファイルの読み込みと書き込み
UTFテキストファイルの読み込みと書き込みは, 標準ファイル入出力クラス(ifstream
, ofstream
)によって行えます. 注意することとして, テキストファイルとしてではなくバイナリファイルとしてファイルを開きます. テキストファイルとして開くと, 予期しないテキスト変換が行われる可能性があります[9][10].
読み込み
ファイルをバイナリモードで開き, string
型文字列に格納します.
std::string readfile(const std::string& path) { std::ifstream stream(path, std::ios::binary); std::stringstream buffer; buffer << stream.rdbuf(); return buffer.str(); }
例
UTFテキストファイルを用意します. 例では, UTF-8ファイルを用意します.
hello_world.utf8
Hello, ワールド! 🎉
読み込みます.
auto utf8 = readfile("hello_world.utf8");
書き込み
string
型文字列をファイルにバイナリモードで書き込みます.
void writefile(const std::string& path, const std::string& string) { std::ofstream stream(path, std::ios::binary); stream.write(string.data(), string.size()); }
例
ソースファイルでUTF-8文字列を用意します[注 1].
std::string utf8("Hello, ワールド! 🎉");
ファイルに書き込みます.
writefile("out.utf8", utf8);
UTF-8, UTF-16, UTF-32の相互変換
UTF-8 < = > UTF-32
out_utf32 = unicode::utf8to32<std::string>(utf8); assert(unicode::utf32to8<std::string>(out_utf32) == utf8);
UTF-16 < = > UTF-32
out_utf16 = unicode::utf32to16<std::string>(out_utf32); assert(unicode::utf16to32<std::string>(out_utf16) == out_utf32);
UTF-16 < = > UTF-8
assert(unicode::utf16to8<std::string>(out_utf16) == utf8); assert(unicode::utf8to16<std::string>(utf8) == out_utf16);
UTF-8, UTF-16, UTF-32文字列の各文字(コードポイント)ごとのイテレート
UTF-8文字列のイテレート
iterate_utf8
関数を使用します.
/** * @param iter * Iterator with an iterate size of 1 byte. * After function execution, this iterator points to the first code unit of the next code point. * * @param end * The end iterator of the string. * * @param strict * If enabled, raise an exception when an illegal character is found. * * @return code point */ template<typename Iter8> uint32_t iterate_utf8(Iter8& iter, const Iter8& end, bool strict = true);
例
// UTF-8文字列を用意 std::string utf8("Hello, ワールド! 🎉"); { // 結果出力のためのログ用意 logging::InfoStream info(__FILE__, __LINE__); for (auto iter = utf8.begin(); iter != utf8.end(); ) { // 一文字分進め, そのコードポイントを出力 info << std::hex << unicode::iterate_utf8(iter, utf8.end()) << " "; } }
- 出力
INFO : 48 65 6c 6c 6f 2c 20 30ef 30fc 30eb 30c9 21 20 1f389
UTF-16文字列のイテレート
iterate_utf16(iter, end)
関数を使用します.
/** * @param iter * Iterator with an iterate size of 2 bytes. * After function execution, this iterator points to the first code unit of the next code point. * * @param end * The end iterator of the string. * * @param strict * If enabled, raise an exception when an illegal character is found. * * @return code point */ template<typename Iter16> uint32_t iterate_utf16(Iter16& iter, const Iter16& end, bool strict = true);
例
// UTF-8文字列を用意 std::string utf8("Hello, ワールド! 🎉"); // UTF-16文字列に変換. `wstring`型に格納 auto utf16 = unicode::utf8to16<std::wstring>(utf8); { // 結果出力のためのログ用意 logging::InfoStream info(__FILE__, __LINE__); for (auto iter = utf16.begin(); iter != utf16.end();) { // 一文字分進め, そのコードポイントを出力 info << std::hex << unicode::iterate_utf16(iter, utf16.end()) << " "; } }
- 結果
INFO : 48 65 6c 6c 6f 2c 20 30ef 30fc 30eb 30c9 21 20 1f389
UTF-32文字列のイテレート
UTF-32文字列は, 一文字当たり4バイトの固定長のため, 標準イテレータでそのままイテレートします.
例
// UTF-8文字列を用意 std::string utf8("Hello, ワールド! 🎉"); // UTF-32文字列に変換. `u32string`型に格納 auto utf32 = unicode::utf8to32<std::u32string>(utf8); { // 結果出力のためのログ用意 logging::InfoStream info(__FILE__, __LINE__); // そのままイテレート for (auto c : utf32) { info << std::hex << c << " "; } }
- 結果
INFO : 48 65 6c 6c 6f 2c 20 30ef 30fc 30eb 30c9 21 20 1f389
UTF-8文字置き換え
本ライブラリでは, イテレート機能に独自のイテレータを使用せず, 標準のイテレータを使用しているため, 標準クラスの機能と連携できます. ここでは, string
型文字列クラスに用意されているfind
とreplace
を使用して文字の置き換えをしてみます.
例
文字列に含まれている🎉
を🍣
に置き換える.
// UTF-8文字列を用意 std::string utf8("Hello, ワールド! 🎉"); // 変換前のコードポイントを見る { logging::InfoStream info(__FILE__, __LINE__); for (auto iter = utf8.cbegin(); iter != utf8.cend(); ) { info << std::hex << unicode::iterate_utf8(iter, utf8.cend()) << " "; } } // 変換する { // 検索する auto pos = utf8.find("🎉"); if (pos != utf8.npos) { // 見つけた場合 logging::InfoStream(__FILE__, __LINE__) << "Find tada!"; // 見つけた場所のイテレータを取得 auto begin = utf8.begin() + pos; auto iter = begin; // 1コードユニット分イテレータを進める unicode::iterate_utf8(iter, utf8.end()); // "tada!"のコードユニット数を見る logging::InfoStream(__FILE__, __LINE__) << "tada size = " << iter - begin << " bytes"; logging::InfoStream(__FILE__, __LINE__) << "Replace tada with sushi."; // begin~iter間の文字"tada!"を"sushi"に置き換える utf8.replace(begin, iter, "🍣"); } } // 変換後のコードポイントを見る { logging::InfoStream info(__FILE__, __LINE__); for (auto iter = utf8.cbegin(); iter != utf8.cend(); ) { info << std::hex << unicode::iterate_utf8(iter, utf8.cend()) << " "; } }
- 結果
INFO : 48 65 6c 6c 6f 2c 20 30ef 30fc 30eb 30c9 21 20 1f389 INFO : Find tada! INFO : tada size = 4 bytes INFO : Replace tada with sushi. INFO : 48 65 6c 6c 6f 2c 20 30ef 30fc 30eb 30c9 21 20 1f363
仕組み
UTF-8, UTF-16, UTF-32の相互変換の方法は, 以下のページで解説しています.
注釈
- ^ ソースコードをUTF-8で保存し, コンパイラに認識させる必要があります.
参考文献
- ^ a b "Usage statistics of character encodings for websites". W3Techs. accessed at 2021-04-05.
- ^ "basic_string". cpprefjp. accessed at 2021-05-10.
- ^ "string_view". cpprefjp. accessed at 2021-05-10.
- ^ "codecvt_utf8". cpprefjp. accessed at 2021-05-10.
- ^ "codecvt_utf16". cpprefjp. accessed at 2021-05-10.
- ^ "codecvt_utf8_utf16". cpprefjp. accessed at 2021-05-10.
- ^ a b c "UTF-8エンコーディングされた文字の型としてchar8_tを追加". cpprefjp. accessed at 2021-05-10.
- ^ "char16_tとchar32_tの文字・文字列リテラルを、文字コードUTF-16/32に規定". cpprefjp. accessed at 2021-05-10.
- ^ "Difference between opening a file in binary vs text [duplicate]". stack overflow. accessed at 2021-05-11.
- ^ a b "fopen". C++ Reference. accessed at 2021-05-11.
- ^ "File IO in C++ (text and binary files)". CodeingUnit. accessed at 2021-05-11.