2015 年 7 月

Volume 30 Number 7

C++ - Win32 API の境界で STL 文字列を使用する

Giovanni Dicanio | 2015 年 7 月

Win32 API は、純粋な C インターフェイスを使用していくつか機能を公開しています。つまり、Win32 API の境界でテキストを交換する場合に、C++ の文字列クラスをネイティブに利用できない機能があります。このような場合は、C スタイルの文字ポインターをそのまま使用します。たとえば、Win32 SetWindowText 関数には、次のプロトタイプがあります (関連 MSDN ドキュメント、bit.ly/1Fkb5lw、英語、より抜粋)。

BOOL WINAPI SetWindowText(
  HWND hWnd,
  LPCTSTR lpString
);

文字列パラメーターは、LPCTSTR の形式で表現します。この LPCTSTR は const TCHAR* に相当します。Unicode のビルド (Visual Studio 2005 以降では既定で、最新の Windows C++ アプリケーションでの使用が推奨されるビルド) では、TCHAR typedef が wchar_t に相当するため、SetWindowText のプロトタイプは次のようになります。

BOOL WINAPI SetWindowText(
  HWND hWnd,
  const wchar_t* lpString
);

基本的に、入力文字列は定数 (つまり、読み取り専用の) wchar_t 文字ポインタとして渡します。このとき、ポインターが指す文字列は、従来の純粋な C スタイルの NUL 終端の文字列であると想定されます。これが、Win32 API の境界で渡される入力文字列の典型的なパターンです。

一方、Win32 API の境界での出力文字列は、通常、呼び出し側が確保した出力バッファーへのポインターと、呼び出し側が用意したバッファーの合計サイズを表すサイズ パラメーターの 2 種類の情報を使用して表現します。GetWindowText 関数 (bit.ly/1bAMkpA、英語) がその一例です。

int WINAPI GetWindowText(
  HWND hWnd,
  LPTSTR lpString,
  int nMaxCount
);

この場合、出力先の文字列バッファーに関連する情報 ("出力" 文字列パラメーター) は、最後の 2 つのパラメーター、lpString と nMaxCount に格納します。前者の lpString は出力先の文字列バッファーへのポインターです。このポインターは LPTSTR Win32 typedef を使用して表現します。これは、TCHAR* (Unicode ビルドの場合は wchar_t*) に変換されます。後者の nMaxCount は、出力先文字列バッファーの合計サイズを wchar_ts で表現します。この値には、終端の NUL 文字を含みます (こうした C スタイルの文字列は、NUL 終端の文字列配列です)。

もちろん、純粋な C ではなく C++ を使用すると、ユーザー モードの Windows コード、特に Windows アプリケーションを開発する際の生産性が極めて高くなります。実際一般的には、C++ を使用すると、アプリケーションのパフォーマンスに悪影響を及ぼすことなく、コードのセマンティック レベルが上がり、プログラマの生産性が向上します。特に、C スタイルの NUL 終端文字列配列をそのまま扱うよりも、便利な C++ 文字列クラスを使用する方が格段に有利です (簡単で生産性が高く、バクを引き起こす確率も低くなります)。

さて、ここで問題になるのは、Win32 API 層の操作に使用でき、純粋な C インターフェイスをネイティブに公開するのは、どの C++ 文字列クラスかということです。

アクティブ テンプレート ライブラリ (ATL)/Microsoft Foundation Class (MFC) ライブラリの CString: ATL/MFC の CString クラスが選択肢の 1 つです。CString は、ATL、MFC、Windows Template Library (WTL) などの C++ Windows フレームワークと非常に適切に統合されていて、C++ を使用した Win32 プログラミングを簡略化します。したがって、これらのフレームワークを使用する場合、Win32 API プラットフォーム固有の C++ Windows アプリケーション層においては、CString を使って文字列を表現することが理にかなっています。さらに、CString は、リソースから文字列を読み込む機能など、Windows プラットフォーム固有の便利な機能も提供します。これらはプラットフォーム固有の機能で、標準テンプレート ライブラリ (STL) などのクロスプラットフォーム標準ライブラリでは基本的に実現できない機能です。したがって、たとえば、既存の ATL クラスや MFC クラスから派生した新しい C++ クラスの設計と実装が必要な場合は、CString を使用して文字列を表現することを強くお勧めします。

標準 STL の文字列: ただし、Windows アプリケーションを構成する独自に設計した C++ クラスのインターフェイスで、標準の文字列クラスを使用する方がよいケースがあります。たとえば、独自に設計する C++ クラスのパブリック インターフェイスにおいて、CString のような Windows 固有のクラスではなく、STL 文字列クラスを選択し、C++ コード内のできるだけ早い段階で Win32 API 層を抽象化する場合です。そこで、STL の文字列クラスに格納されているテキストのケースを考えてみます。この場合は、(今回の冒頭で説明したように、純粋な C インターフェイスを公開する) Win32 API の境界をまたがって STL の文字列を渡す必要があります。ATL、WTL、MFC の場合、フレームワークが Win32 C インターフェイス層と CString を "結び付ける" コードを背後で自動的に実装しますが、このような便利な処理は STL の文字列には利用できません。

説明をわかりやすくするために、Windows API の既定の Unicode エンコーディングである Unicode UTF-16 形式で文字列が格納されるものとします。実際、これらの文字列が別の形式 (Unicode UTF-8 など) だった場合は、Win32 API の境界で UTF-16 に変換すれば、前述の要件を満たすことができます。この変換には、Win32 MultiByteToWideChar 関数と WideCharToMultiByte 関数を使用します。MultiByteToWideChar は Unicode UTF-8 エンコード ("マルチバイト") 文字列から Unicode UTF-16 ("ワイド") 文字列への変換に、WideCharToMultiByte はその逆の変換に使用します。

Visual C++ では、std::wstring 型が Unicode UTF-16 文字列の表現に適しています。これは、基盤の文字型が、Visual C++ では UTF-16 コード単位と同じく 16 ビットのサイズになる wchar_t であるためです。ただし、GCC Linux など、その他のプラットフォームでは wchar_t は 32 ビットになります。したがって、そのようなプラットフォームでは、std::wstring を使用して Unicode UTF-32 エンコードされたテキストを表現することになります。このようなあいまいさを取り除くために、C++11 では新しい標準の文字列型として、std::u16string が導入されています。これは、16 ビット文字単位である char16_t 型の要素を使用した std::basic_string クラスの特殊化です。

入力文字列の場合

Win32 API が PCWSTR (以前の用語では LPCWSTR)、つまり、const wchar_t* の NUL 終端の C スタイル入力文字列パラメーターを想定している場合は、単純に std::wstring::c_str メソッドを呼び出して問題ありません。実際、このメソッドは、読み取り専用の NUL 終端の C スタイル文字列へのポインターを返します。

たとえば、ウィンドウのタイトル バーのテキストやコントロールのテキストを std::wstring に格納されているコンテンツを使用して設定する場合、次のように SetWindowText Win32 API を呼び出すことができます。

std::wstring text;
::SetWindowText(hWnd, text.c_str());

ATL/MFC の CString は、文字定数ポインター (const TCHAR*、最新の Unicode ビルドでは const wchar_t* に相当) そのものへの暗黙の変換を行いますが、STL の文字列ではそのような暗黙の変換は行われません。開発者が明示的に STL 文字列の c_str メソッドを呼び出さなければなりません。最新の C++ では、暗黙の変換は問題になることが多いと一般に認められているため、STL の文字列クラスの設計者は、明示的に呼び出し可能な c_str 文字列を採用しています (最新の STL スマート ポインターで暗黙の変換が行われないことについては、bit.ly/1d9AGT4 のブログ記事 (英語) に関連情報があります)。

出力文字列の場合

出力文字列の場合は、もう少し複雑な処理になります。出力文字列を扱う際の通常のパターンでは、まず、Win32 API を呼び出して、出力文字列の出力先バッファーのサイズを取得します。サイズには、終端の NUL 文字が含まれる場合も含まれない場合もあるため、特定の Win32 API のドキュメントを参照して確認してください。

次に、適切なサイズのバッファーを、呼び出し側が動的に確保します。このバッファーのサイズが、前の手順で特定したサイズです。

最後に、もう 1 度 Win32 API を呼び出して、実際の文字列コンテンツを読み取り、呼び出し側が確保したバッファーに格納します。

たとえば、コントロールのテキストを取得するには、GetWindowTextLength API を呼び出してテキスト文字列の長さを wchar_ts に取得します (この場合、返される長さには、終端の NUL 文字は含まれません)。

その後、その長さを基に、文字列バッファーを確保できます。このとき、次のように std::vector<wchar_t> を使用して、文字列バッファーを管理することもできます。

// Get the length of the text string
// (Note: +1 to consider the terminating NUL)
const int bufferLength = ::GetWindowTextLength(hWnd) + 1;
// Allocate string buffer of proper size
std::vector<wchar_t> buffer(bufferLength);

これは "new wchar_t[bufferLength]" をそのまま呼び出すよりも簡単です。"new wchar_t[bufferLength]" の場合は、delete[] を呼び出して適切にバッファーを解放する必要があります (解放しないとメモリ リークが発生します)。new[] をそのまま呼び出す場合と比べて vector はオーバーヘッドがやや多くなりますが、std::vector を使う方が簡単です。実際、std::vector を使う場合、std::vector のデストラクターが自動的に確保したバッファーを削除します。

これは、例外に対して安全な C++ コードをビルドするうえでも役立ちます。コードのどこかで例外がスローされた場合、std::vector のデストラクターが自動的に呼び出されます。一方、new[] を使ってバッファーを動的に確保した場合、ポインターはそのまま格納されて保持されるため、リークが発生することになります。

std::vector に代わるもう 1 つの方法としては、std::unique_ptr を使用する方法があります。具体的には、std::unique_ptr< wchar_t[] > を使用します。この方法では、(std::unique_ptr のデストラクターのおかげで) std::vector の自動破棄 (と例外に対して安全な) 機能を利用できるほか、std::unique_ptr は保持しているポインターそのものの非常に小さな C++ ラッパーになるため、std::vector よりもオーバーヘッドが小さくなります。基本的に unique_ptr は、安全な RAII 境界内で保持しているポインターを保護します。RAII (https://ja.wikipedia.org/wiki/RAII) は、非常によく利用される C++ プログラミングのイディオムです。念のために説明しておくと、RAII は、unique_ptr のデストラクターなど、ラップされたポインターに対して自動的に delete[] を呼び出し、関連付けられているリソースを解放して、メモリー リーク (リソース リーク全般) を防止するための実装テクニックです。

unique_ptr を使う場合、このコードは、次のようになります。

// Allocate string buffer using std::unique_ptr
std::unique_ptr< wchar_t[] > buffer(new wchar_t[bufferLength]);

または、(C++14 以来提供されていて、Visual Studio 2013 に実装されている) std::make_unique を、次のように使用します。

auto buffer = std::make_unique< wchar_t[] >(bufferLength);

次に、適切なサイズのバッファーを確保し、使用できる状態になったら、その文字列バッファーへのポインターを渡して、GetWindowText API を呼び出します。std::vector によって管理されるバッファーそのものの先頭へのポインターを取得するには、次のように std::vector::data メソッド (https://msdn.microsoft.com/ja-jp/library/dd647618.aspx) を使用します。

// Get the text of the specified control
::GetWindowText(hWnd, buffer.data(), bufferLength);

unique_ptr の場合、代わりに自身の get メソッドを呼び出すことができます。

// Get the text of the specified control (buffer is a std::unique_ptr)
::GetWindowText(hWnd, buffer.get(), bufferLength);

最後に、一時バッファーにあるコントロールのテキストのディープ コピーを作成して std::wstring インスタンスに格納します。

std::wstring text(buffer.data()); // When buffer is a std::vector<wchar_t>
std::wstring text(buffer.get()); // When buffer is a std::unique_ptr<wchar_t[]>

上記のコード スニペットでは、wstring のコンストラクター オーバーロードを使用して、NUL 終端入力文字列への定数 wchar_t ポインターをそのまま取得しています。これは、呼び出される Win32 API によって、呼び出し側が用意した出力先文字列バッファーに NUL 終端文字が挿入されるため、問題なく機能します。

少しばかりの最適化として、(wchar_ts に格納されている) 文字列の長さ が既知の場合、ポインターと文字列の文字カウント パラメーターを受け取る wstring コンストラクター オーバーロードを使用することもできます。この場合、文字列長は呼び出し側で提供されるため、wstring コンストラクターによって検出する必要はありません (Visual C++ 実装での wcslen 呼び出しなど、通常は O(N) 操作を使用します)。

出力の場合のショートカット: std::wstring によるインプレース処理

std::vector (または std::unique_ptr) を使用して一時文字列バッファーを確保し、そのディープ コピーを std::wstring に格納する部分にはショートカットを利用できます。

基本的に、std::wstring のインスタンスを直接出力先バッファーに使用して、Win32 API に渡すことができます。

実際、std::wstring には、適切なサイズの文字列を作成するために使用できる resize メソッドがあります。この場合、サイズを調整する文字列の実際の最初のコンテンツは、呼び出される Win32 API によって上書きされるため、気にする必要はありません。図 1 に、std::wstring を使用してインプレースで文字列を読み取る方法を示したサンプル コード スニペットを示します。

図 1 のコードに関して、いくつかポイントを説明します。

図 1 std::wstring を使用した文字列のインプレース読み取り

// Get the length of the text string
// (Note: +1 to consider the terminating NUL)
const int bufferLength = ::GetWindowTextLength(hWnd) + 1;
// Allocate string of proper size
std::wstring text;
text.resize(bufferLength);
// Get the text of the specified control
// Note that the address of the internal string buffer
// can be obtained with the &text[0] syntax
::GetWindowText(hWnd, &text[0], bufferLength);
// Resize down the string to avoid bogus double-NUL-terminated strings
text.resize(bufferLength - 1);

内部文字列バッファーへの書き込みアクセスの取得: まず、GetWindowText 呼び出しから見て行きます。

::GetWindowText(hWnd, &text[0], bufferLength);

C++ プログラマなら、std::wstring::data メソッドを使用して、GetWindowText 呼び出しにポインターを渡すかたちで、内部文字列コンテンツにアクセスすることを考えるかもしれません。しかし、wstring::data から返される定数ポインターでは、内部文字列バッファーのコンテンツを変更することはできません。また、GetWindowText では wstirng のコンテンツへの書き込みアクセスを想定しているため、その呼び出しはコンパイルされません。そこで代わりの方法としては、&text[0] 構文を使用して内部文字列バッファーの先頭アドレスを取得し、出力 (つまり、変更可能な) 文字列として、目的の Win32 API に渡します。

この手法では最初にバッファーを確保し、次にディープ コピーを std::wstring に格納します。そのため、最終的に破棄される一時 std::vector がないので、前の方法と比べて効率的です。実際、この場合は、コードは std::wstring インスタンス内でインプレース処理されます。

不適切な NUL 終端文字列の重複回避: 図 1 のコードの最後の行に注目します。

// Resize down the string to avoid bogus double-NUL-terminated strings
text.resize(bufferLength - 1);

最初の wstring::resize 呼び出し (text.resize(bufferLength);、"-1" の補正なし) では、GetWindowText Win32 API が NUL 終端文字を書き込めるだけのサイズが内部 wstring バッファーに割り当てられています。ただし、この GetWindowText によって書き込まれる NUL 終端文字に加えて、また別の NUL 終端文字が std::wstring によって暗黙のうちに追加されます。その結果、文字列は 2 つの NUL 終端文字がある文字列になります。1 つは GetWindowText によって書き込まれた NUL 終端文字で、もう 1 つは wstring によって自動的に追加された NUL 終端文字です。

この不適切な NUL 終端文字列の問題を解決するには、wstring インスタンスのサイズを調節して、Win32 API が追加する NUL 終端文字列を削除し、wstring の NUL 文字列のみが保持されるようにします。これが、text.resize(bufferLength-1) 呼び出しの目的です。

競合状態への対処

まとめに入る前に、一部の API で発生する可能性がある競合状態に対処する方法について説明しておきます。たとえば、Windows レジストリから文字列値を読み取るコードがあるとします。C++ プログラマなら、前のセクションで紹介したパターンに従って、最初に RegQueryValueEx 関数を呼び出して、文字列値の長さを取得し、次にその文字列のバッファーを確保して、最後にもう一度 RegQueryValueEx を呼び出して実際の文字列値を読み取り、前の手順で作成したバッファーに格納するでしょう。

この場合、最初と最後の RegQueryValueEx 呼び出しの間に、別のプロセスによって文字列値が変更されると、競合状態が発生する可能性があります。最初の呼び出しによって返された文字列長は、別のプロセスによってレジストリに書き込まれた新しい文字列値とは無関係なため、無意味な値になる可能性があります。RegQueryValueEx への 2 回目の呼び出しでは、不適切なサイズで確保されたバッファーに、新しい文字列が読み取られることになります。

この問題を解決するには、図 2 のようなコーディング パターンを使用します。

図 2 文字列の取得時に発生する可能性がある競合状態に対処するサンプル コーディング パターン

LONG error = ERROR_MORE_DATA;
std::unique_ptr< wchar_t[] > buffer;
DWORD bufferLength = /* Some initial reasonable length for the string buffer */;
while (error == ERROR_MORE_DATA)
{
  // Create a buffer with bufferLength size (measured in wchar_ts)
  buffer = std::make_unique<wchar_t[]>(bufferLength);
  // Call the desired Win32 API
  error = ::SomeApiCall(param1, param2, ..., buffer.get(), &bufferLength);
}
if (error != ERROR_SUCCESS)
{
  // Some error occurred
  // Handle it e.g. throwing an exception
}
// All right!
// Continue processing
// Build a std::wstring with the NUL-terminated text
// previously stored in the buffer
std::wstring text(buffer.get());

図 2 のように while ループを使用することで、適切なサイズのバッファーに文字列を読み取ることができます。ループ内では毎回 ERROR_MORE_DATA が返されるため、API 呼び出しが成功する (ERROR_SUCCESS が返される) か、バッファーのサイズが不適切であるという理由以外でエラーになるまで、適切な bufferLength 値を使って新しいバッファーを確保します。

図 2 のコード スニペットは、サンプルのスケルトン コードにすぎません。他の Win32 API では、ERROR_INSUFFICIENT_BUFFER コードなど、呼び出し側による不適切なサイズのバッファー確保に関して、別のエラー コードが使用される可能性があります。

まとめ

Win32 API の境界で (ATL/WTL や MFC などのフレームワークを利用して) CString を使用することで、Win32 の純粋な C インターフェイス層との相互運用のしくみが隠ぺいされますが、C++ プログラミングでは、STL 文字列を使用する場合に、細かい点で注意が必要なことがあります。今回は、STL wstring クラスと Win32 の純粋な C インターフェイス機能を相互運用するためのコーディング パターンをいくつか説明しました。入力の場合は、wstring の c_str メソッドを使用して、Win32 C インターフェイス境界で、簡単な定数 (読み取り専用) の NUL 終端文字列文字ポインターとして、入力文字列を渡すだけで済みます。出力文字列の場合は、一時文字列バッファーを呼び出し側が確保する必要があります。これは、std::vector STL クラスか、それよりもややオーバーヘッドが少ない STL std::unique_ptr スマート ポインター テンプレート クラスを使用して、実現できます。その他の方法としては、wstring::resize メソッドを使用して、文字列インスタンス内に Win32 API 関数の出力先バッファーを割り当てることもできます。この場合は、呼び出される Win32 API が NUL 終端文字を書き込めるだけの領域を指定したうえで、サイズを切り下げてその終端文字を削除し、wstring の NUL 終端文字だけが残されるようにします。最後に、発生する可能性がある競合状態について説明し、その競合状態を解決するためのサンプル コーディング パターンを紹介しました。


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

この記事のレビューに協力してくれた、David Cravey (GlobalSCAPE) と Stephan T. Lavavej (Microsoft) に心より感謝いたします。
David Cravey は、GlobalSCAPE のエンタープライズ アーキテクトです。いくつかの C++ ユーザー グループのまとめ役であり、4 度 Visual C++MVP を受賞しています。
Stephan T. Lavavej は、マイクロソフトのシニア開発者です。2007 年より、Dinkumware 社と協力して、Visual C++ の C++ 標準ライブラリの実装のメンテナンスに携わっています。また、make_unique や透過的な演算子ファンクタなど、いくつかの C++14 機能も設計しています。