次の方法で共有



September 2016

Volume 31 Number 9

C++ - STL の文字列クラスと Win32 API による Unicode エンコーディングの変換

Giovanni Dicanio

Unicode は、最新のソフトウェアで多言語テキストを表現するための業界標準です。Unicode コンソーシアムの公式 Web サイト (bit.ly/1Rtdulx、英語) によると、「Unicode とは、プラットフォーム、プログラム、言語を問わず、すべての文字に一意の数値を指定するもの」です。 この一意の各数値をコード ポイントと呼び、通常、プレフィックス「U+」の後に 16 進形式の一意数値を続けて表記します。たとえば、文字「C」 に関連付けられるコード ポイントは U+0043 です。Unicode は、表意文字を含め、世界中の書記体系の大部分を網羅する業界標準です。たとえば、「学習」や「知識」などの意味を持つ日本語の漢字「学」は、コード ポイント U+5B66 に関連付けられます。現在、Unicode 標準では、1,114,000 を超えるコード ポイントが定義されています。

抽象的なコード ポイントから実際のビット列へ: UTF-8 エンコーディングと UTF-16 エンコーディング

ただし、コード ポイントは抽象概念です。プログラマにとっての問題は、「この Unicode コード ポイントをコンピューターのビット列で具体的にどのように表現するか」です。 この疑問の答えに直接結び付くのが、Unicode エンコーディングの概念です。Unicode エンコーディングとは、基本的には、Unicode コード ポイントの値をビット列で表現するために特別に定義された方法です。Unicode 標準では複数のエンコーディングが定義されていますが、中で最も重要なのが UTF-8 と UTF-16 です。この 2 つは、可能性のあるすべての Unicode 「文字」、つまりコード ポイントをエンコードできる可変長エンコーディングです。したがって、この 2つのエンコーディング間で変換を行っても失われるものはありません。 つまり、変換処理中に失われる Unicode 文字はありません。

UTF-8 はその名前が示すとおり、コード単位として 8 ビットを使用します。これは、2 つの重要な特性を念頭に設計されました。1 つは ASCII との下位互換です。有効な ASCII 文字コードを UTF-8 を使ってエンコードしてもバイト値は変わりません。言い換えれば、有効な ASCII テキストは、自動的に有効な UTF-8 エンコード済みテキストになります。

もう 1 つはエンディアンによる複雑さが存在しないことです。これは、UTF-8 でエンコードされた Unicode テキストが、8 ビットのバイト単位のシーケンスになるためです。UTF-8 エンコーディングは (UTF-16 とは異なり)、仕様上、エンディアンの影響を受けません。これは、異なるコンピューティング システム間でテキストを交換する場合に重要な特性になります。コンピューティング システムは、そのハードウェア アーキテクチャにより、エンディアンが異なる可能性があります。

こうした Unicode の 2 つの特性を踏まえると、大文字の C (コード ポイント U+0043) は、UTF-8 では 1 バイトの 0x43 (16 進数の 43) にエンコードされます。このコードは (UTF-8 から ASCII への下位互換によって) 文字 C に関連付けられる ASCII コードと一致します。これに対して、日本語の漢字「学」 (コード ポイント U+5B66) は、UTF-8 では 3 バイトのシーケンス「0xE5 0xAD 0xA6」にエンコードされます。

UTF-8 はインターネットで最も多く使用されている Unicode エンコーディングです。W3Techs の最新統計 ( bit.ly/1UT5EBC、英語) によると、UTF-8 は分析した Web サイト全体の 87% で使われています。

UTF-16 は、基本的には Unicode 対応の Windows API が使用する業界標準のエンコーディングです。UTF-16 は、Windows 以外の多くのソフトウェア システムでも「ネイティブ」な Unicode エンコーディングです。たとえば、Qt、Java、International Components for Unicode (ICU) ライブラリなどは、UTF-16 エンコーディングを使用して Unicode 文字列を格納します。

UTF-16 が使用するコード単位は 16 ビットです。UTF-8 と同様、UTF-16 も可能性のある Unicode コード ポイントをすべてエンコードできます。ただし、UTF-8 は有効な Unicode コード ポイントを 8 ビットのバイト単位で 1 ~ 4 つのコードにエンコードするのに対して、UTF-16 はもっとシンプルです。UTF-16 では、Unicode コード ポイントは 16 ビットのコード単位で 1 つまたは 2 つのコードにエンコードされます。ですが、コード単位が 1 バイトよりも大きくなるため、エンディアンの複雑さが持ち込まれます。実際、ビッグエンディアンの UTF-16 とリトルエンディアンの UTF-16 があります (UTF-8 エンコーディングはエンディアンとは無関係で 1 つしかありません)。

Unicode では、65,536 (216) 個のコード ポイントの連続したグループとして面の概念を定義します。最初の面を、0 面または基本多言語面 (BMP) といいます。ほぼすべての近代言語の文字や多くの記号は BMP に配置されます。BMP に配置されるすべての文字は、16 ビットのコード単位を 1 つ使用する UTF-16 で表現されます。

補助文字は BMP 以外の面に配置されます。補助文字には、絵文字などの絵記号、エジプトのヒエログリフのような歴史的な筆記文字が含まれます。BMP 外の補助文字は、UTF-16 では 16 ビットのコード単位 2 つにエンコードされます。この 2 つのコードはサロゲート ペアとも呼ばれます。

大文字の C (U+0043) は、UTF-16 では、1 つの 16 ビットのコード単位 0x0043 にエンコードされます。漢字の「学」 (U+5B66) は、UTF-16 では、1 つの 16 ビットのコード単位 0x5B66 にエンコードされます。多くの Unicode 文字では、コード ポイントによる「抽象」表現 (U+5B66 など) が 16 進形式の UTF-16 エンコーディング (16 ビット文字 0x5B66 など) に直接対応付けられています。

ちょっとしたお楽しみとして、いくつかの絵文字記号を見てみましょう。Unicode 文字の「雪だるま」 (U+2603) は、UTF-8 では 3 バイトのシーケンス 0xE2 0x98 0x83 にエンコードされますが、UTF-16 エンコーディングでは 1つの 16 ビット単位 0x2603 にエンコードされます。 Unicode 文字「ビール ジョッキ」 (U+1F37A) は BMP 外に配置されており、UTF-8 では 4 バイトのシーケンス 0xF0 0x9F 0x8D 0xBA にエンコードされます。UTF-16 エンコーディングではこの文字が 2 つの 16 ビットのコード単位 0xD83C 0xDF7A にエンコードされます。これは UTF-16 サロゲート ペアの一例です。

Win32 API を使用した UTF-8 と UTF-16 との変換

ここまで説明してきたように、Unicode テキストは、コンピューターのメモリ内部では特定の Unicode エンコーディングを基に異なるビット列で表現されています。では、どのエンコーディングを使用すべきでしょう。 その答えは 1 つではありません。

最近の通説によると、クロスプラットフォーム C++ コードでは、UTF-8 でエンコードされている Unicode テキストを std::string クラスのインスタンス内に格納するのが優れたアプローチとされています。さらに、アプリケーションの境界や異なるコンピューターをまたがってテキストを交換する場合は、UTF-8 がエンコーディングの選択肢になることが広く認められています。それは、UTF-8 がエンディアンの影響を受けない形式であるためです。どのような場合でも、少なくとも Win32 API の境界では UTF-8 と UTF-16 の変換が必要になります。それは、Unicode 対応の Windows API がネイティブ エンコーディングとして UTF-16 を使用しているためです。

そこで、Unicode UTF-8/UTF-16 エンコーディング変換を実装する C++ コードについて考えます。そのためには、MultiByteToWideChar と WideCharToMultiByte という、ペアとなる 2 つの主要 Win32 API を使用します。 前者は、UTF-8 (API 用語では「マルチバイト」文字列) から UTF-16 (「ワイド文字」文字列) に変換します。後者はこれとは逆の変換を行います。2 つの Win32 関数はインターフェイスと使用パターンが似ているため、ここでは MultiByteToWideChar のみを紹介します。ただし、もう一方の APIを使用する C++ 互換コードも本稿付属のダウンロードから入手できます。

標準の STL 文字列クラスを使用した Unicode テキストの格納: 本稿は C++ に関するコラムなので、なんらかの文字列クラスに Unicode テキストを格納することが想定されます。そこで疑問になるのが、 「Unicode テキストの格納にはどのような種類の C++ 文字列クラスが使えるか」です。 答えは、Unicode テキストに使用されているエンコーディングによって異なります。UTF-8 エンコーディングが使用されている場合、8 ビットのコード単位がベースになるため、C++ で各コード単位を表現するのにシンプルな文字を使用できます。この場合は、UTF-8 でエンコードされた Unicode テキストを格納するため、char ベースの STL std::string クラスが選択肢として有力です。

一方、Unicode テキストが UTF-16 でエンコードされている場合、各コード単位は 16 ビットのワードで表現されます。Visual C++ では、wchar_t 型のサイズはまさに 16 ビットです。そのため、UTF-16 Unicode テキストを格納する場合は wchar_t をベースとする STL std::wstring クラスが適しています。

C++ 標準は wchar_t のサイズを指定していないため、Visual C++ コンパイラでは 16 ビットになりますが、他の C++ コンパイラはさまざまなサイズを自由に使用しています。実際、Linux の GNU GCC C++ コンパイラによって定義される wchar_t のサイズは 32 ビットです。wchar_t 型はコンパイラやプラットフォームによってサイズが異なるため、この型をベースとする std::wstring クラスは移植可能ではありません。つまり、Windows で Visual C++ コンパイラ (wchar_t のサイズは 16 ビット) を使用する場合は UTF-16 エンコードされた Unicode テキストの格納に wstring を使用できますが、Linux の GCC C++ コンパイラでは wchar_t 型が 32 ビットという異なるサイズで定義されるため使用できません。

実際には、知名度が低く使用される機会も少ない UTF-32 という Unicode エンコーディングも存在します。 その名が示すとおり、このエンコーディングは 32 ビットのコード単位を基本とします。したがって、GCC/Linux の 32 ビット wchar_t は、Linux プラットフォームでの UTF-32 エンコーディングの候補になります。

このように wchar_t のサイズがあいまいなため、結果として、それに基づく C++ コード (std::wstring クラス自体を含む) は移植可能ではなくなります。一方、char ベースの std::string は移植可能です。ただし、実践的観点からは、wstring を使用した UTF-16 エンコード テキストの格納は、Windows 固有の C++ コードのみに適していることになります。実際、この部分のコードは既に Win32 API を操作していますが、当然、この Win32 API は仕様上プラットフォーム固有です。そのため、wstring をさらに加えても、状況は変わりません。

最後に、UTF-8 と UTF-16 はどちらも可変長エンコーディングなので、一般に string::length メソッドや wstring::length メソッドの戻り値は、文字列に格納されている Unicode 文字数 (コード ポイント数) と対応しないことも意識しなければなりません。

変換関数インターフェイス: では、UTF-8 エンコードされた Unicode テキストを UTF-16 でエンコードされた同じテキストに変換する関数を考えてみましょう。たとえば、UTF-8 エンコードされた Unicode 文字列を std::string クラスを使用して格納するクロスプラットフォーム C++ コードがあるとします。このテキストを Unicode 対応の Win32 API (通常、UTF-16 エンコーディングを使用) にテキストを渡す場合、この関数が役に立ちます。このコードは Win32 API と対話するため、既に移植は不可能です。そのため、ここで UTF-16 テキストを格納するには std::wstring が適しています。考えられる関数のプロトタイプは、以下のようになります。

std::wstring Utf8ToUtf16(const std::string& utf8);

この変換関数は、標準の STL std::string に格納されている UTF-8 エンコード文字列を入力として受け取ります。これは入力パラメーターなので、const 参照 (const &) によって関数に渡します。変換の結果として、UTF-16 でエンコードされた文字列を返し、std::wstring インスタンスに格納します。ただし、Unicode エンコーディングの変換中に問題が起こる可能性があります。たとえば、入力の UTF-8 文字列に無効な UTF-8 シーケンスが含まれる場合があります (このシーケンスは、コードの他の部分でのバグが原因であったり、一部の悪意のある動作の結果としてもたらされた可能性があります)。このような場合、セキュリティの観点からは、危険なバイト シーケンスを利用するよりも、変換を中断する方が適切です。今回の変換関数は、C++ 例外をスローすることで、無効な UTF-8 入力シーケンスの問題に対処します。

変換のエラーに対する例外クラスの定義: Unicode エンコーディング変換が失敗した場合、どのような種類の C++ クラスを例外のスローに使用できるでしょうか。 1 つの選択肢として、std::runtime_error など、標準ライブラリで既に定義されているクラスの使用が考えられます。ですが、今回は新しいカスタム C++ 例外クラスを std::runtime_error から派生して定義します。MultiByteToWideChar のような Win32 API が失敗したときは、GetLastError を呼び出して、失敗の原因についての詳細情報を入手します。たとえば、入力文字列内に無効な UTF-8 シーケンスが見つかった場合、GetLastErorr から返される一般的なエラー コードは ERROR_NO_UNICODE_­TRANSLATION です。後々デバッグに便利なので、このような情報をカスタム C++ 例外クラスに追加することに意味があります。この例外クラスの定義は以下のように始めます。

// utf8except.h
#pragma once
#include <stdint.h>   // for uint32_t
#include <stdexcept>  // for std::runtime_error
// Represents an error during UTF-8 encoding conversions
class Utf8ConversionException
  : public std::runtime_error
{
  // Error code from GetLastError()
  uint32_t _errorCode;

GetLastError から返される値は、32 ビット符号なし整数を表す DWORD 型です。ただし、DWORD は、Win32 固有の移植可能ではない typedef です。この C++ 例外クラスが C++ コードの Win32 固有の部分からスローされるとしても、キャッチするのはクロスプラットフォーム C++ コードになる可能性があります。 そのため、Win32 固有の typedef の代わりに移植可能な typedef (uint32_t など) を使用します。

次に、このカスタム例外クラスのインスタンスをエラー メッセージとエラー コードで初期化するコンストラクターを、以下のように定義します。

public:
  Utf8ConversionException(
    const char* message,
    uint32_t errorCode
  )
    : std::runtime_error(message)
    , _errorCode(errorCode)
  { }

最後に、エラー コードを読み取り専用アクセスにするため、パブリックの get アクセス操作子を定義します。

uint32_t ErrorCode() const
  {
    return _errorCode;
  }}; // Exception class

このクラスは std::runtime_error から派生しているため、what メソッドを呼び出して、コンストラクターで渡されたエラー メッセージを取得できます。このクラスの定義では移植可能な標準要素のみを使用しているため、C++ コードのクロスプラットフォーム部分が Windows 固有のスロー ポイントから離れて配置されていても、問題なく使用できます。

UTF-8 から UTF-16 に変換する: MultiByteToWideChar の動作

これで、変換関数のプロトタイプを定義し、UTF-8 変換エラーを適切に示す C++ 例外クラスを実装しました。ここからは、変換関数の本体を考えます。既にお分かりかもしれませんが、UTF-8 から UTF-16 への変換は、MultiByte­ToWideChar Win32 API を使用して行います。「マルチバイト」と「ワイド文字」という用語には歴史的背景があります。基本的に、この API とペアとなる WideCharToMultiByte は、特定のコード ページで格納されたテキストと Unicode テキストとの間で変換を行うことが本来の目的でした。この場合の Unicode テキストは、Unicode 対応の Win32 API で UTF-16 エンコードを使用するテキストです。ワイド文字とは wchar_t のことです。したがって、wchar_t ベースの文字列 (UTF-16 でエンコードされた文字列) に関連付けられています。これに対し、マルチバイト文字列とは、コード ページで表現されるバイトのシーケンスです。コード ページの古い概念は、後に UTF-8 エンコーディングを含むように拡張されました。

この API の代表的な使用パターンでは、結果の文字列のサイズを取得するため、最初に Multi­ByteToWideChar を呼び出します。次に、取得したサイズ値に応じて、文字列バッファーを割り当てます。変換先が UTF-16 文字列の場合、通常 std::wstring::resize メソッドを使用してこれを行います。詳細については、2015 年 7 月号のコラム「Win32 API の境界で STL 文字列を使用する」(msdn.com/magazine/mt238407) を参照してください。 最後に、MultiByteToWideChar をもう一度呼び出し、確保した変換先文字列バッファーを使用して、実際のエンコーディング変換を実行します。同じ使用パターンは、ペアになる WideCharToMultiByte API にも当てはまります。

では、カスタム Utf8ToUtf16 変換関数内部で、このパターンを C++ コードで実装してみましょう。まず、空の入力文字列を受け取る特殊なケースを処理します。この場合は、空の出力 wstring を返すだけです。

#include <Windows.h> // For Win32 APIs
#include <string>    // For std::string and std::wstring
std::wstring Utf8ToUtf16(const std::string& utf8)
{
  std::wstring utf16; // Result
  if (utf8.empty())
  {
    return utf16;
  }

変換フラグ: MultiByteToWideChar の初回呼び出しで、変換先の UTF-16 文字列のサイズを取得します。この Win32 関数のインターフェイスはやや複雑で、いくつかのフラグに応じて動作を定義します。この API は変換関数 Utf8ToUtf16 本体でもう一度呼び出すため、両方の呼び出しで使用できる名前付き定数を定義します。これは、コードの可読性と保守性にとっても優れた手法です。

// Safely fails if an invalid UTF-8 character
// is encountered in the input string
constexpr DWORD kFlags = MB_ERR_INVALID_CHARS;

セキュリティの観点からは、入力文字列に無効な UTF-8 シーケンスが見つかった場合、変換プロセスを失敗させることも優れた手法です。MB_ERR_INVALID_CHARS フラグの使用は、Michael Howard と David LeBlanc の共著『WRITING SECURE CODE 第2版』(Microsoft Press、2003 年) でも推奨されています。

constexpr キーワードをサポートしない古いバージョンの Visual C++ コンパイラをプロジェクトで使用している場合、そのコンテキストで static const を代わりに使います。

文字列長と size_t から int への安全な変換: MultiByteToWideChar は、入力文字列の length パラメーターが int 型で表現されていると想定します。しかし、STL の文字列クラスの length メソッドは、size_t に相当する型の値を返します。64 ビット ビルドでは、Visual C++ コンパイラが警告を出力して、size_t (8 バイト) から int (4 バイト) への変換によるデータ損失の可能性を示唆します。しかし、size_t と int がどちらも Visual C++ コンパイラによって 32 ビット整数と定義される 32 ビット ビルドでも、size_t は符号なしで、int は符号付きのため不一致が起こります。これはある程度の長さの文字列では問題になりませんが、長さが (231 - 1) よりも長い (サイズが 20 億バイトを超える) 文字列だとそうはいきません。この場合、符号なし整数 (size_t) から符号付き整数 (int) への変換によって負の数が生成される可能性があり、負の長さは意味をなさないためです。

そこで、utf8.length を呼び出してソースの UTF-8 入力文字列のサイズを取得して、そのサイズを MultiByteToWideChar API を渡すのではなく、size_t 値の実際の長さをチェックし、int に安全かつ有意に変換できることを確認した後にのみ MultiByteToWideChar API に渡すようにします。

以下のコードを使用して、size_t の長さが int 型変数の最大値を超えないことを確認し、超えている場合は例外をスローします。

if (utf8.length() > static_cast<size_t>(std::numeric_limits<int>::max()))
{
  throw std::overflow_error(
    "Input string too long: size_t-length doesn't fit into int.");
}

ここでは (<limits> C++ 標準ヘッダーの) std::numeric_limits クラス テンプレートを使用して、int 型の最大値をクエリしています。ただし、このコードは実際にはコンパイルされない場合があります。それはどんな場合でしょう。 問題なのは、Windows プラットフォーム SDK ヘッダーにある min マクロと max マクロの定義です。特に、max プリプロセッサ マクロの Windows 固有の定義は、std::numeric_limits<int>::max メンバー関数の呼び出しと矛盾しています。これを防ぐ方法はいくつかあります。

有効な解決策としては、<Windows.h> をインクルードする前に #define NOMINMAX を指定します。これによって、Windows 固有のプリプロセッサ min/max マクロの定義を無効にします。ただし、これらのマクロ定義を無効にすると、実際には、<gdiplus.h> など、Windows 固有のマクロ定義を必要とする他の Windows ヘッダーで問題が起こる可能性があります。

そこで、もう 1 つの選択肢として、メンバー関数 std::numeric_limits::max を追加のかっこで囲み、前述のマクロ展開を防ぐ方法があります。

if (utf8.length() > static_cast<size_t>((std::numeric_limits<int>::max)()))
{
  throw std::overflow_error(
    "Input string too long: size_t-length doesn't fit into int.");
}

また、代替策として、C++ std::numeric_limits クラス テンプレートの代わりに定数 INT_MAX を使用する方法もあります。

どの方法を使っても、サイズ チェックを終え、length 値が int 型の変数に適しているとわかれば、static_cast による size_t から int へのキャストは安全に実行されます。

// Safely convert from size_t (STL string's length)
// to int (for Win32 APIs)
const int utf8Length = static_cast<int>(utf8.length());

UTF-8 文字列の長さの単位は、8 ビット char 単位、つまりバイト単位です。

API の初回呼び出し: 変換後の文字列長の取得: ここで、以下の MultiByteToWideChar の初回呼び出しを行い、変換後の UTF-16 文字列の長さを取得します。

const int utf16Length = ::MultiByteToWideChar(
  CP_UTF8,       // Source string is in UTF-8
  kFlags,        // Conversion flags
  utf8.data(),   // Source UTF-8 string pointer
  utf8Length,    // Length of the source UTF-8 string, in chars
  nullptr,       // Unused - no conversion done in this step
  0              // Request size of destination buffer, in wchar_ts
);

初回は、最後の引数に 0 を渡して呼び出します。これにより、MultiByteToWideChar API が、変換後の文字列に必要なサイズを返すように指示します。変換は行われません。変換後の文字列のサイズは、wchar_ts (8 ビット文字ではない) で表現されます。これは変換後の文字列が、16 ビット wchar_ts のシーケンスで構成される UTF-16 エンコードされる文字列になるため適切です。

入力 UTF-8 std::string のコンテンツを読み取り専用アクセスで取得するには、std::string::data メソッドを呼び出します。UTF-8 文字列の長さを入力パラメーターとして明示的に渡してるため、上記のコードは、NUL が内部に埋め込まれている std::string のインスタンスでも機能します。

定数 CP_UTF8 を使用して、入力文字列が UTF-8 でエンコードされていることを指定します。

エラーの場合の処理: 無効な UTF-8 シーケンスが入力文字列に存在する場合など、先ほどの関数呼び出しに失敗すると、MultiByteToWideChar API は 0 を返します。この場合、Win32 関数 GetLast­Error を呼び出して、エラーの原因についての詳細情報を取得します。通常、無効な UTF-8 文字の場合に返されるエラー コードは ERROR_NO_UNICODE_TRANSLATION です。

エラーの場合は、例外をスローします。これは、先ほど独自に設計した Utf8Conversion­Exception クラスのインスタンスにすることができます。

if (utf16Length == 0)
{
  // Conversion error: capture error code and throw
  const DWORD error = ::GetLastError();
  throw Utf8ConversionException(
    "Cannot get result string length when converting " \
    "from UTF-8 to UTF-16 (MultiByteToWideChar failed).",
    error);
}

変換後の文字列用のメモリ割り当て: Win32 関数の呼び出しに成功すると、要求した変換後の文字列の長さがローカル変数 utf16Length に格納されます。そのため、この長さを使用して、出力 UTF-16 文字列の変換先メモリを割り当てます。std::wstring クラスのインスタンスに格納されている UTF-16 文字列の場合は、resize メソッドを呼び出すだけです。

utf16.resize(utf16Length);

入力 UTF-8 文字列の長さを MultiByteToWideChar に明示的に渡しているため、Win32 API は変換後の文字列に NUL 終端文字を追加しません。API に -1 を渡して NUL 終端文字が見つかるまで入力文字列をスキャンするよう求めた場合は、追加されます。 API は、明示的に渡された length 値によって指定された入力文字列の正確な文字数を処理するだけです。したがって、「utf16Length + 1」値を指定して std::wstring::resize を呼び出す必要はありません。 Win32 API が NUL 終端文字を付加しないため、変換後の std::wstring 内部に終端文字用の余地を作る必要はありません (詳しくは、2015 年 7 月号のコラムを参照してください)。

API の 2 回目の呼び出し: 実際の変換の実行: これで、UTF-16 wstring インスタンスには変換後の UTF-16 エンコード テキストを収容できるスペースを確保できました。ここで MultiByteToWideChar を再度呼び出して、実際に変換されたビット列を変換後の文字列に取得します。

// Convert from UTF-8 to UTF-16
int result = ::MultiByteToWideChar(
  CP_UTF8,       // Source string is in UTF-8
  kFlags,        // Conversion flags
  utf8.data(),   // Source UTF-8 string pointer
  utf8Length,    // Length of source UTF-8 string, in chars
  &utf16[0],     // Pointer to destination buffer
  utf16Length    // Size of destination buffer, in wchar_ts          
);

「&utf16[0]」構文を使用して、std::wstring の内部メモリバッファーへの書き込みアクセスを取得します (こちらの詳細についても、2015 年 7 月号のコラムを参照してください)。

MultiByteToWideChar への初回呼び出しが成功した場合は、2 回目の呼び出しが失敗する可能性はほぼありません。それでも、API の戻り値をチェックすることは、非常に適切で安全なコーディング手法です。

if (result == 0)
{
  // Conversion error: capture error code and throw
  const DWORD error = ::GetLastError();
  throw Utf8ConversionException(
    "Cannot convert from UTF-8 to UTF-16 "\
    "(MultiByteToWideChar failed).",
    error);
}

2 回目の呼び出しが成功したら、変換後の UTF-16 文字列が、最終的に呼び出し元に返されます。

 

return utf16;
} // End of Utf8ToUtf16

使用例: (C++ のクロスプラットフォーム コードなど) UTF-8 エンコードされた Unicode 文字列があり、その文字列を Unicode 対応の Win32 API に渡す場合、今回のカスタム変換関数を以下のように呼び出します。

std::string utf8Text = /* ...some UTF-8 Unicode text ... */;
// Convert from UTF-8 to UTF-16 at the Win32 API boundary
::SetWindowText(myWindow, Utf8ToUtf16(utf8Text).c_str());
// Note: In Unicode builds (Visual Studio default) SetWindowText
// is expanded to SetWindowTextW

関数 Utf8ToUtf16 は、UTF-16 エンコードされた文字列を含む wstring インスタンスを返します。このインスタンスの c_str メソッドを呼び出し、NUL 終端文字列への C スタイルのロウ ポインターを取得して、そのポインターを Unicode 対応の Win32 API に渡します。

UTF-16 から UTF-8 への逆変換コードよく似ており、その場合は WideCharToMultiByte API を呼び出します。前述のように、UTF-8 と UTF-16 との間の Unicode 変換は損失が起こらず、変換の過程で文字が失われることはありません。

Unicode エンコーディング変換ライブラリ

互換性のある C++ コードのサンプルは、本稿付属のダウンロード可能なアーカイブに含めています。32 ビット ビルドと 64 ビット ビルドの両方で、Visual C++ の警告レベル 4 (/W4) で明確にコンパイルしているため、このコードは再利用可能です。コードは、ヘッダーのみの C++ ライブラリとして実装されます。基本的に、この Unicode エンコーディング変換モジュールは、utf8except.h と utf8conv.h という 2 つのヘッダー ファイルで構成されます。utf8except.h には、Unicode エンコーディング変換の間にエラー状態を通知する C++ 例外クラスの定義を含めています。utf8conv.h は、実際の Unicode エンコーディング変換関数を実装します。

utf8except.h はクロスプラットフォーム C++ コードのみを含め、C++ プロジェクト内のどこでも UTF-8 エンコーディング変換の例外をキャッチできるようにしています。Windows 固有ではないコード部分を含め、仕様上クロスプラットフォーム C++ を使用しています。これに対して、utf8conv.h は Win32 API の境界を直接操作するため、Windows 固有のコードを含みます。

プロジェクトでコードを再利用するには、先ほどのヘッダー ファイルを #include するだけです。ダウンロード可能なアーカイブには、他にもいくつかのテスト ケースを実装する追加のソース ファイルを含めています。

まとめ

Unicode は、最新のソフトウェアで多言語テキストを表現するための業界標準です。Unicode テキストは、さまざまな形式でエンコードできます。 中で最も重要な 2 つの形式が UTF-8 と UTF-16 です。ほとんどの場合、C++ Windows コードでは、Unicode 対応の Win32 API はネイティブな Unicode エンコーディングとして UTF-16 を使用するため、UTF-8 と UTF-16 との間で変換が必要になります。UTF-8 テキストは STL std::string クラスのインスタンスに適切に格納されます。これに対し、std::wstring は Visual C++ コンパイラをターゲットにする Windows C++ コードで UTF-16 エンコードされたテキストを格納するのに適しています。

Win32 API の MultiByteToWideChar と WideCharTo­MultiByte を使用して、UTF-8 エンコーディングと UTF-16 エンコーディングで表現される Unicode テキストを相互に変換できます。今回は、MultiByteTo­WideChar API の使用パターンについて詳しく説明しました。この API を再利用可能な最新の C++ ヘルパー関数にラップして UTF-8 から UTF-16 に変換しました。逆変換もほぼ同じパターンに従います。逆変換を実装する再利用可能な C++ コードは本稿付属のダウンロードから入手できます。


Giovanni Dicanio は、C++ と Windows を専門とするコンピューター プログラマであり、Pluralsight の執筆者です。また、Visual C++ MVP でもあります。プログラミングとコース作成の傍ら、C++ 専門のフォーラムやコミュニティで他の開発者をサポートしています。彼の連絡先は、giovanni.dicanio@gmail.com (英語のみ) です。彼のブログは、blogs.msmvps.com/gdicanio (英語) です。

この記事のレビューに協力してくれた技術スタッフの David Carvey と Marc Gregoire に心より感謝いたします。
David Cravey は、GlobalSCAPE のエンタープライズ アーキテクトです。いくつかの C++ ユーザー グループのまとめ役であり、4 度 Visual C++MVP を受賞しています。

Marc Gregoire はベルギー出身のシニア ソフトウェア エンジニアであり、Belgian C++ Users Group の創設者であり、『Professional C++』(Wiley、2014 年) の著者であり、『C++ Standard Library Quick Reference』(Apress、2016 年) の共著者でもあります。さらに、彼はさまざまな書籍のテクニカル ディレクターを務め、2007 年以降は、彼の VC++ の専門知識について MVP を毎年受賞しています。彼の連絡先は、marc.gregoire@nuonsoft.com (英語のみ) です。