syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

先輩、今年もよろしくお願いしますね


【Fate】「先輩、今年もよろしくお願いしますね」イラスト/sygh[sai] [pixiv]

年末に体調を崩してなかなか年賀絵を描けなかったんですが、ずっと妄想していた和装の桜を描くことができてひとまず肩の荷が降りました。
本年もぼちぼちコードを書いたりイラストや漫画を描いたりしていく所存ですのでよろしくお願いいたします。

和服の花柄は出来合いのテクスチャを使うのが嫌だったので、Illustratorで自作しました。美遊のサファイアちゃんも去年Illustratorで線画を作成してみましたが、ベクトル画像は拡大縮小や回転にいくらでも耐えてくれるので、プログラムのように部品化しやすいのがメリットです。


【プリズマ☆イリヤ】「黒魔法少女」イラスト/sygh[sai] [pixiv]

CUDA Warpシャッフル命令のエミュレーション

今更ですがせっかくCompute Capability 3.0対応のKepler世代グラフィックスカードを手に入れたので、CUDAのWarpシャッフル命令の動作テストを兼ねて、代替機能をエミュレートする関数を書いてみました。
Visual Studio 2012、CUDA 6.5、GeForce GTX 770で動作確認済み。

#define WARP_SIZE (32)

typedef unsigned int uint;

// HLSL の SV_GroupIndex 相当。
static __device__ __forceinline__ int GetLocalThreadGroupFlatIndex()
{
  return threadIdx.x + blockDim.x * threadIdx.y + blockDim.x * blockDim.y * threadIdx.z;
}

static __device__ __forceinline__ int GetGlobalThreadFlatIndex()
{
  const int blockId = blockIdx.x + gridDim.x * blockIdx.y + gridDim.x * gridDim.y * blockIdx.z;
  return blockId * blockDim.x * blockDim.y * blockDim.z + GetLocalThreadGroupFlatIndex();
}

#pragma region // Emulation Functions //

// 書き込み後の同期だけでなく、読み込み後の同期も必要。でないと安全に共有メモリ上の古いデータを上書き/破棄できない。

template<typename T> static __device__ T EmulateShuffle(T x, int srcLane)
{
  extern __shared__ T sharedMem[];
  const int localIndex = GetLocalThreadGroupFlatIndex();
  const int laneBlockId = localIndex / WARP_SIZE;
  const int laneId = srcLane & (WARP_SIZE - 1);
  sharedMem[localIndex] = x;
  __syncthreads();
  const T y = sharedMem[laneBlockId * WARP_SIZE + laneId];
  __syncthreads();
  return y;
}

template<typename T> static __device__ T EmulateShuffleUp(T x, uint delta)
{
  extern __shared__ T sharedMem[];
  const int localIndex = GetLocalThreadGroupFlatIndex();
  const int laneBlockId = localIndex / WARP_SIZE;
  const int laneId = localIndex & (WARP_SIZE - 1);
  const int shiftAmt = laneId - static_cast<int>(delta);
  sharedMem[localIndex] = x;
  __syncthreads();
  const T y = (0 <= shiftAmt) ? sharedMem[laneBlockId * WARP_SIZE + shiftAmt] : sharedMem[localIndex];
  __syncthreads();
  return y;
}

template<typename T> static __device__ T EmulateShuffleDown(T x, uint delta)
{
  extern __shared__ T sharedMem[];
  const int localIndex = GetLocalThreadGroupFlatIndex();
  const int laneBlockId = localIndex / WARP_SIZE;
  const int laneId = localIndex & (WARP_SIZE - 1);
  const int shiftAmt = laneId + static_cast<int>(delta);
  sharedMem[localIndex] = x;
  __syncthreads();
  const T y = (shiftAmt <= (WARP_SIZE - 1)) ? sharedMem[laneBlockId * WARP_SIZE + shiftAmt] : sharedMem[localIndex];
  __syncthreads();
  return y;
}

template<typename T> static __device__ T EmulateShuffleXor(T x, int laneMask)
{
  extern __shared__ T sharedMem[];
  const int localIndex = GetLocalThreadGroupFlatIndex();
  const int laneBlockId = localIndex / WARP_SIZE;
  const int laneId = localIndex & (WARP_SIZE - 1);
  sharedMem[localIndex] = x;
  __syncthreads();
  const T y = sharedMem[laneBlockId * WARP_SIZE + ((laneId ^ laneMask) & (WARP_SIZE - 1))];
  __syncthreads();
  return y;
}

#pragma endregion

エミュレート関数はそれぞれ、下記のCUDA組み込み関数に相当します。当然組み込み関数を使ったほうが手軽だし高速ですが、CC 3.0以降のデバイスでないと使えません。

// 指定したレーン (Warp 内のスレッド番号) の var の値を受け取ります。
T __shfl(T var, int srcLane, int width = warpSize);

// 指定した数 delta だけ離れたスレッドの var の値を受け取ります。
// up は指定した数だけ上の番号のスレッドから、down は下の番号のスレッドから受け取ります。
// Warp の外に出てしまった場合は自身のスレッドでの値が返ってきます。
T __shfl_up(T var, unsigned int delta, int width = warpSize);
T __shfl_down(T var, unsigned int delta, int width = warpSize);

// 自分のレーン番号と指定した laneMask を XOR した結果の番号のスレッドから受け取ります。
// laneMask ごとにレジスタの中身を交換することができます。
T __shfl_xor(T var, int laneMask, int width = warpSize);

CUDAの場合、共有メモリのサイズ指定はコンパイル時だけでなく、下記のように(ハードウェア仕様上限が許す限り)実行時に指定することもできます。今回のエミュレーション関数は共有メモリにexternを指定することで、実行時にサイズ指定する方法を選びました。

const dim3 gridSize(16, 4, 4);
const dim3 blockSize(64, 8, 2);
const size_t elemCount = gridSize.x * gridSize.y * gridSize.z * blockSize.x * blockSize.y * blockSize.z;
const size_t sharedMemSize = blockSize.x * blockSize.y * blockSize.z * sizeof(TargetType);
…
kernelFunc<<<gridSize, blockSize, sharedMemSize>>>(devOutArray, devInArray);

一応動作確認テストはしましたが、完全に同一仕様でエミュレートできているかどうかは未保証です。ご利用の際は自己責任でどうぞ。

CUDAはNVIDIA専用という最大のネックがあるので、個人的にDirectComputeに比べてあまり好きではないのですが、CUDA自体は非常に効率よく最高水準のGPGPUプログラムを記述できる、地球上でもっとも優れた超並列プログラミング環境であることは確かです。C++erとして特に気に入っているのは、Visual C++などのホストコンパイラーにコンパイルさせるホスト関数だけでなく、NVCCコンパイラーにコンパイルさせるデバイス関数でもクラス、テンプレートやC++11 Featureがほとんどそのまま使える点です(CUDA 7ではC++11対応が強化されるそうです)。HLSLでもクラスは使えますが、CUDAのプログラマビリティはHLSLを遥かに凌駕します。GLSLコンピュートシェーダーやOpenCL-Cごときではまったく勝負にならないレベルです。
なおネイティブC++から使えるGPGPU言語拡張としては、Visual C++ 2012以降のC++ AMPも強力です。WindowsXbox Oneのほか、すでにLinux向けにもオープンソース実装されているので、OpenCLの代わりにC++ AMPを使ってみてもよいでしょう。

ちなみに自分はPC用グラフィックスカードに関しては、G7xからG8x、Fermi、Keplerと乗り換えてきましたが、ずっとNVIDIAオーナーです。AMDはハードウェアに関しては良いモノを作っていることは確かなんですが(ゲーム向けも業務向けもHPC向けも性能に関してはAMDのほうが上)、ドライバーの完成度がお話にならないレベルであったり、ソフトウェアを軽視しているメーカーだと感じます。また、APUを除き、現行のグラフィックスカード単体での省電力性能は、ハイエンドでもNVIDIAに分があります。Keplerを使い倒したら、いずれMaxwellへ、さらにその後はPascalへ移行する予定ですが、AMDが今の劣悪な状況を改善しようとしないかぎり、今後も個人でAMDグラフィックスを購入することはないでしょう(逆に言うとドライバー/ソフトウェア面さえ改善されれば、オープン標準規格のOpenCLC++ AMPを推進するAMDNVIDIAよりも良い選択肢になり得ます)。

VS 2008のWPF-MFC相互運用

(これは2011-03-18に書いた故OCNブログの記事を移植したものです)

以下を参考に、MFC アプリから WPF を使おうとして、Visual Studio 2008 SP1 のバグに遭遇しました。
WPF ユーザー コントロールを HwndSource 経由でホストする Win32 アプリ……といいつつ、結局 C++/CLI による中継が必要になりますのでご注意。
なお、VS 2010 では C++/CLI のインテリセンスが効かないという致命的なダウングレードがあるため、MFC-WPF の相互運用をするならば、VS 2008/2012/2013 を使うことをお勧めします。

バグ現象

スクリーンショット (a) は、WPF ユーザー コントロール プロジェクトと、それを使う予定の MFC クライアント アプリ プロジェクトを格納したソリューション。

(a)
https://sygh-jp.github.io/content_hosting/my_program_ss/Wpf_Designer_Error_ss_2010_05_31a.png

一見何の変哲も無いが、ここで C# コードのタブ (.xaml.cs) へ移動し、WPF のプロジェクトをリビルドする。そして、XAML デザイナのタブ (.xaml) へ再度移動する。
すると、スクリーンショット (b) のように再読み込みを促す情報バーが出るので、クリックすると、スクリーンショット (c) のように IDE がクラッシュする。

(b)
https://sygh-jp.github.io/content_hosting/my_program_ss/Wpf_Designer_Error_ss_2010_05_31b.png

(c)
https://sygh-jp.github.io/content_hosting/my_program_ss/Wpf_Designer_Error_ss_2010_05_31c.png

再度ソリューションを開くと、スクリーンショット (d) のエラーが待っている。

「オブジェクト参照がオブジェクト インスタンスに設定されていません。」

なん……だと……

いわゆる NullReferenceException のハンドルされない例外が発生している。よくあるアプリケーションが落ちる原因のひとつだが、こいつが WPF デザイナー(Visual Studio IDE プロセス)内部で発生している。

(d)
https://sygh-jp.github.io/content_hosting/my_program_ss/Wpf_Designer_Error_ss_2010_05_31d.png

なんぞこれ……ということで調べてみたところ、MS のサポートページにひっそりと修正プログラムが公開されていた。

上記によると、Win32 プロジェクトがスタートアップ プロジェクトとして設定されているとバグるらしい(ソリューション エクスプローラーにおいてプロジェクト名がボールド体になっているものがスタートアップ)。
確かにスクリーンショット (e) のように、WPF のほうをスタートアップ プロジェクトに設定して、リビルドすると先ほどのエラーは発生しない。

(e)
https://sygh-jp.github.io/content_hosting/my_program_ss/Wpf_Designer_Error_ss_2010_05_31e.png

ともかく、修正プログラム 963035 を適用せよ、とのこと。下記へ飛ぶ。

修正プログラムは下記のサイトにアップロードされている、とのこと。なんというたらいまわし

このサイトの Downloads タブに目的の修正プログラム「VS90SP1-KB963035-x86.exe」のダウンロード リンクがある。
プログラム適用によって問題が一応修正されていることを確認。
開発者にとっては非常に重要な修正プログラムですが、Windows Update 経由では適用されません。また、2014年現在、修正プログラムへのリンクが切れているようです。代替の修正プログラムが公開されているのかどうかは不明。

しかし Win32 アプリをスタートアップにするとバグるっていう理由が意味不明です。WPF との相互運用を図るとき、普通にやる組み合わせだと思うんですが……最低限そのくらいはテストしてからリリースして欲しいです。

余談:Win32 アプリから WPF を使う3つの方法

  1. /clr を使って C++/CLI でホストコードを書く。
  2. /clr を使って C++/CLI でホストコードを書く。ただし C++/CLI のコードは DLL に押し込む。
  3. WPF アセンブリを COM 公開する。

1番目は自明なので説明は不要ですね。

2番目の方法を具体的に説明すると、/clr を有効にした Win32 DLL、MFC 拡張 DLL もしくは MFC 標準 DLL 内で WPF を使用して、ネイティブ MFC インターフェイスもしくは C 言語形式関数を公開する形にしておけば OK。例えば、ネイティブの HWND ウィンドウ ハンドルを受け取って WPF コントロールのホストとして使う C 言語形式関数を C++/CLI で実装して、WPF コントロールインスタンスを DLL 内で gcnew するようにラップしてしまえば、ネイティブ C++ で書かれた EXE(厳密に言うと、/clr を無効にした EXE)から普通に呼び出せます。ただし /clr を使う場合は CRT/MFC/ATL を静的リンクではなく動的リンクする必要があるので注意しましょう。

1番目および2番目の方法において、もし混合クラスのメンバーとしてマネージ オブジェクトを保持したい場合は、ラッパークラス テンプレートの gcroot を使います。

ちなみに C++/CLI だと、ネイティブ/マネージのうちどちらか片方のインターフェイスだけでなく、両方のインターフェイスを合わせ持つ混合アセンブリ(COM でいうデュアル I/F に近い)まで作れてしまいます。つまり、ネイティブ Win32/MFC 向けの関数/クラスと、C# or VB.NET 向けのクラス両方をひとつのアセンブリに含めるという変態的なことができてしまう。

C++/CLI を一切使いたくない or 使えない場合、3番目の方法として、WPF アセンブリを COM コンポーネントとして公開する方法があります。COM 公開するには、regasm でレジストリ登録する方法のほか、マニフェストで Side-by-Side アセンブリにする方法があるようです。

ATL で開発した従来の COM コンポーネント DLL に関しては、マニフェストで Side-by-Side アセンブリ化(プライベート アセンブリ化)し、実運用にも十分耐えうることを確認したことがあるんですが、WPF アセンブリに関しては未検証です。

もし純粋なネイティブ Win32 アプリ(従来のネイティブ MFCVB 6.0 などで開発したアプリ)から直接 WPF アセンブリを使いたい場合は、2番目もしくは3番目の方法を使う必要があります。ただし、境界面のやりとりに手間や労力がかかることは覚悟しておく必要があります。

CButtonとCMFCButtonの比較

(これは2010-06-20に書いた故OCNブログの記事を移植したものです)

Visual Studio 2008 SP1にて導入された、MFC Feature Pack付属のCMFCButtonは、従来のCButtonに比べてかなり強力です。微妙に使い勝手が違う部分もありますが、ボタンの文字色変更とか、画像とテキスト両方表示したいときとかに、わざわざオーナードローなんかしたくないって人は重宝するんじゃないかと。
とはいえ、.NETのWindows FormsやWPFに比べたら、それでも面倒なことには変わりないです。もしどうしてもMFCを使わないといけない理由がない限り、今後はWPFを使うべきでしょう。

とりあえずサンプルを下記に置いておきます。

CButton、CMFCButtonともに、XPとVista/7、そしてビジュアル スタイルとクラシック スタイルで外見が大きく異なるボタンがあることに注意。違いを生み出しているのはコモンコントロールのバージョン(クラシックはVer.5.x、XPのビジュアルはVer.6.0、Vista/7のビジュアルはVer.6.1)ですが、このあたりはGUI開発者にとってかなり悩ましいところです。BS_BITMAPを適用して(BitmapプロパティがTrue)、WS_EX_STATICEDGEを適用して(Static EdgeプロパティがTrue)、CButton::SetBitmap()でビットマップを割り当てたボタンに至っては、なんかもう別物(;´д`)っていうか相当残念な感じになってます。

あと、以前から気になっていたんですが、チェックボックスラジオボタンをプッシュボタン形式にしてビジュアル スタイルを適用すると、かなり表示がキモいです。プッシュボタン形式というのは、BS_PUSHLIKEが適用された状態(Push LikeプロパティがTrue)のことです。Windowsのボタンはマウスでクリックしたまま離さないでいると、押し込んでいる状態を表す感じで外観が変わりますが、あのステートを使ってチェック状態を表してるつもりの変なヤツです。トグルボタンとも呼ばれます。
さらに、ビジュアル スタイルでチェックON状態のプッシュボタン形式チェックボックスにマウスオーバーすると、キモさ倍増です。自分はWindowsのクラシック スタイルが大嫌いなので、XP/Vistaでも常にビジュアル スタイルを適用していますが、いくらなんでもこれだけはクラシック スタイルのほうがON/OFFの状態が分かりやすい。サンプルではフォントを変えたり色を変えたりして、少しでも分かりやすくしようと努めてますけど、無駄な努力っていうか見苦しいだけ。MSはXPやVistaのリリース前に、一般ユーザーにビジュアル スタイルのUIをテストしてもらわなかったんでしょうか? コンシューマーゲームとかだったら、UIとか操作性は必ず一般ユーザーの反応をチェックすると思うんですが。

以下でもXP Visual Styleのトグルボタンにてホバーしたときの外観の問題点について述べられています。ちなみにtable内のspan要素のborderスタイルのoutset/insetによってunselected/selectedの外観が比較されていますが、Firefoxでは正しく表示されないようです。
devblogs.microsoft.com

なので、自分はプッシュボタン形式のチェックボックスラジオボタンはお勧めしません。「どうしても昔ながらのプッシュボタンがいいんだよ!」って方は、SetWindowTheme() APIで意図的にビジュアル スタイルを切る、という方法もありますが……みんなが慣れているであろう普通のチェックボックスラジオボタンにしておいたほうがいいと思います。

ちなみにCMFCButtonのフォントはデフォルトで必ずMS UI Gothicになるようです。親ダイアログのフォントをメイリオとかMeiryo UIとかにしても連動してくれません。親ダイアログのフォントに合わせたかったら、OnInitDialog()とかでSetFont()を呼んで明示的に設定してやる必要があります。

なお、CMFCButton::EnableWindowsTheming()は再描画メッセージを発行しません。なので、呼び出したあとはクライアント全体あるいはアプリのフレーム全体をInvalidateして再描画する必要があります。それにしても、なんでstaticメソッドなんでしょうか……個別のコントロールごとに呼べません。CMFCButtonの実装を見る限り、個別設定するならばpublicメンバー変数(orz*1)のm_bDontUseWinXPTheme(orz*2)にむりやりTRUEを設定すればいいみたいです。

スクリーンショット1(XP、ビジュアル スタイル)
https://sygh-jp.github.io/content_hosting/my_program_ss/mfc_btn_test_ss_xp01.png

スクリーンショット2(XP、クラシック スタイル)
https://sygh-jp.github.io/content_hosting/my_program_ss/mfc_btn_test_ss_xp02.png

スクリーンショット3(Vista/7、ビジュアル スタイル)
https://sygh-jp.github.io/content_hosting/my_program_ss/mfc_btn_test_ss_seven01.png

スクリーンショット4(Vista/7、クラシック スタイル)
https://sygh-jp.github.io/content_hosting/my_program_ss/mfc_btn_test_ss_seven02.png

*1:残念ながらもう慣れましたが、MFCカプセル化の設計が根本的におかしいです。フィールドをpublic/protectedで公開するとか平気でやらかしてくれていますが、どこの素人プログラマーですか……

*2:否定形の名前を変数名に付けるなと言いたいです。m_disablesVisualStyleもしくはm_usesClassicStyleにするべきでしょう。

CScrollViewの代わりにCViewでスクロール

(これは2010-09-07に書いた故OCNブログの記事を移植したものです)

MFCのCScrollViewは便利なんですが、画像編集ソフトのようなものを作ろうとしたとき、いかんせん細かい制御がしづらいというか、CScrollView::SetScrollSizes()などのユーティリティ メンバー関数を呼び出すタイミングが微妙というか、デバイス座標系と論理座標系が入り乱れて混乱するというか、とにかくスクロールは自前でやったほうがかえって楽だったりします。実はそんなに手間じゃありません。

サンプル:
MfcScrlVwTest.zip
(VS 2010 SP1でビルド、Windows XPと7で動作確認済み)

(left, top, right, bottom) = (-4,000, +4,000, +4,000, -4,000)の論理座標空間を、クライアント領域にマッピングしています。また、Y軸はいわゆる下が正となる2D座標系ではなく、座標変換を使って上が正となるように設定しています。ビューポートをいろいろ設定するだけで、拡大縮小・平行移動とスクロール バーの連動が簡単に実装できるんですが、GDIのビューポートは、OpenGLDirect3Dのそれとは毛色がちょっと違うので最初戸惑いました。まぁいまさらGDIっていうのも時代錯誤な感じですが……

で、サンプルのソースはほとんどMfcScrlVwTestView.h/cppしかいじってません。あとはフレームワークが自動生成したコードをビルドするだけで、わりと見栄えのいいアプリになるのはMFC Feature Packの良いところです。実装したのはスクロールの確認と簡単なダブル バッファリングだけで、完全な印刷処理は実装してないので、そのへんはご自由にどうぞ。

なお、ダブル バッファリングにおけるバック バッファ→フロント バッファの転送処理は、転送元と転送先ともにデバイス座標系に戻して行なったほうが分かりやすいです。

Direct2D

CViewでスクロール バーを扱うコツがつかめたら、グラフィックスのレンダリングには可能な限りGDIではなくDirect2Dを使うようにしましょう。Direct2Dは明示的なバック バッファの作成が不要で、EndDraw(もしくはDXGIスワップチェーンをPresentするタイミング)でフリップしてくれます。ちなみにDirect2DはGDIのデバイス コンテキストにもレンダリングできるようになっていますが、GDI/GDI+とは違って、Direct2Dでは直接印刷(プリンターのデバイス コンテキストに描画)できません。ID2D1RenderTarget::EndDraw()メソッドがエラーコードを返します。なので、印刷時のみGDI+で同じ描画処理を実行するようにするか、ビットマップに一旦描画してプリンターに転送する必要があります。ただし、Windows 8で追加されたDirect2D 1.1であれば印刷用のメタデータ出力もサポートしています。

ATL::CPathユーティリティ

(これは2010-10-31に書いた故OCNブログの記事を移植したものです)

Visual C++ 付属の ATL にはわりと便利なユーティリティ クラスがあるんですが、あまり知られてないのでひとつ紹介します。

ATL::CPath は Windows Shell API の Path*関数を薄くラップしたクラスなんですが、ATL::CString とともに Windows アプリでは重宝するかと。
atlpath.h をインクルードすれば、非 ATL/MFC プロジェクトであっても使えるようになります。

#include <atlpath.h>

void AtlPathTest()
{
    const LPCWSTR pAnswerStr[] = { L"No", L"Yes" };
    {
        const CPathW paths[] = {
            CPathW(L"C:\\test_file.txt"),
            CPathW(L"test_file.txt"),
            CPathW(L".\\test_file.txt"),
            CPathW(L"..\\test_file.txt"),
            CPathW(L"\\\\127.0.0.1\\shared\\test_file.txt"),
        };

        for (int i = 0; i < sizeof(paths) / sizeof(*paths); ++i)
        {
            wprintf(L"'%s' is relative path ? %s\n", static_cast<LPCWSTR>(paths[i]), pAnswerStr[paths[i].IsRelative()]);
        }
        // --> N, Y, Y, Y, N
        puts("");
    }

    {
        // '\\' の代わりに '/' を使うと、意図した結果にならない。
        const LPCWSTR srcPath = L"C:\\directory\\test_file.txt";
        CPathW path = srcPath;
        wprintf(L"RemoveFileSpec('%s') = \n", srcPath);
        path.RemoveFileSpec();
        wprintf(L"'%s'\n\n", static_cast<LPCWSTR>(path)); // 'C:\directory'
    }

    {
        const LPCTSTR srcPath = _T("C:\\directory\\test_file.txt");
        CPath path = srcPath;
        _tprintf(_T("StripPath('%s') = \n"), srcPath);
        path.StripPath();
        _tprintf(_T("'%s'\n\n"), static_cast<LPCTSTR>(path)); // 'test_file.txt'

        path = srcPath;
        _tprintf(_T("RemoveExtension('%s') = \n"), srcPath);
        path.RemoveExtension();
        _tprintf(_T("'%s'\n\n"), static_cast<LPCTSTR>(path)); // 'C:\directory\test_file'
    }

    {
        const LPCSTR path1 = "C:\\directory";
        const LPCSTR path2 = "test_file.txt";
        CPathA path = path1;
        path += path2;
        printf("'%s' + '%s' = \n", path1, path2);
        printf("'%s'\n\n", static_cast<LPCSTR>(path)); // 'C:\directory\test_file.txt'
    }
}

CString は C++ 標準の std::string や std::wstring に比べて、速度面やメモリ効率面においてかなり最適化されているし、ANSIUnicode 変換も簡単にできて、COM 用の _bstr_t との相互変換もサポートされているなど、VBDelphi の文字列に引けを取らない機能を持っているので、ATL/MFC 開発ではまず CString をいかに使いこなせるか、が重要になってきます。ちなみに MFC と ATL は有償ライブラリなので、無償版(Express エディション)の Visual C++ では使えません*1。また、Windows 以外の OS や Visual C++ 以外のコンパイラーへの移植性を考えると、標準 C/C++ ライブラリの使い方も習得しておいたほうがよいでしょう。なお、Unicode 版の CStringW に関しては Windows ストア アプリ開発でも使えますが、MBCS 版の CStringA はデスクトップ アプリ専用です。

あと実際のデバッグで役に立つ機能といえば、ATLTRACE() と ATLASSERT() マクロです。MFC には同等機能として TRACE() と ASSERT() というマクロが存在しますが、ATL 版は非 MFC プロジェクトでも使えるので便利です。ちなみに標準 C ライブラリの assert() マクロは、MSVC 実装ではアサーション失敗時のプログラム停止位置が DebugBreak() 呼び出しまでさかのぼってしまうため使いづらいので、可能であれば ATL 版や MFC 版のアサート、もしくは CRT 版の _ASSERTE() を使うと楽になります*2

*1:Visual Studio 2013 以降は小規模チームや個人開発に限り無償で利用可能な Community エディションが用意されていて、Community エディションでは Professional エディション以上と同様に ATL/MFC を使うことができます。

*2:Visual Studio 2013 では改善されていて、assert() マクロでもアサーションが失敗した位置で停止するようになっています。

WinSDKおよびATLのCOMユーティリティ

(これは2010-11-23に書いた故OCNブログの記事を移植したものです)

MSXMLなどのCOMタイプ ライブラリを#importしたときなど、COMを扱うときによく見かけるのが、Visual C++ CRTヘルパーの _bstr_t クラスと _variant_t クラス (comutil.h) です。それぞれ、COMのBSTR文字列のラッパーと、VARIANT型のラッパーとなっています。
ただしC++プログラマーとしてはこれらがグローバル名前空間で定義されてやがることに殺意を覚えます。シンボル名がアンダースコアで始まっているのは、処理系依存のデータ型であることを誇示しているのか……
あとアンダースコアなしのエイリアス bstr_tvariant_t がtypedefじゃなく#defineなのも情けないです。これらのエイリアスは使わないほうがよいでしょう。MSの#define好きは異常*1

似たようなラッパーはATL (atlcomcli.h) にも用意されていて、ATL::CComBSTRATL::CComVariantが該当するのですが、ついでにATL::CComSafeArray (atlsafe.h) なんかもあります。MFC専用だとCOleVariantというのもあります。

なお、_variant_tは、MFCライブラリでもたまに使われているのを見かけます(CMFCPropertyGridProperty::SetValue()/GetValue()とか)。

正直VC++ネイティブで文字列を扱う場合、他の環境への移植性を考慮しないで良いのであれば、迷わずCStringA/CStringW/CStringを使うのがベストです*2。そもそもテンプレート機能まである静的型付け言語C++で何が悲しくてVariantなんぞ使わないといけないのかとも思いますが、VBスクリプト言語との相互運用を考慮して設計されたCOMデュアル インターフェイスの境界では、たとえ非効率でもBSTRやVARIANT、SAFEARRAYを使わざるを得ません。で、COMのBSTRは::SysAllocString()/::SysFreeString()、SAFEARRAYは::SafeArrayCreate()/::SafeArrayDestroy()でヒープ管理されるので、こういうRAIIラッパーがないと発狂するでしょう。ただ、ラッパークラスはCRTアロケータを使っていない時点で速度が出ない(とくに文字列の結合が毎回再割り当てを伴うために遅いらしい)し、下手な使い方をするとメモリーリークの原因になるので、COMのRAIIラッパーを使用する場合はCOM境界のみに限定するべきです。ちなみにATL::CComBSTRはアタッチ・デタッチがやや特殊で、使い方を誤ると簡単にメモリーリークするらしいので、極力_bstr_tを使ったほうが無難らしいです。

実装を確認してみたところ、一応 CComBSTR::operator& にはアサーションが入っているので、デバッグビルドであれば誤った使い方をしたときに気付くはずです。

*1:Win32 APIでも、enumを使うべき所なのにやたら#define使っているのがかなりうっとうしいです。あと悪名高いmin/maxマクロも後世に語り継がれる大ポカでしょう。

*2:C++標準のstd::string/wstringより最適化されていて高機能です。Windowsの文字列リソースを直接読み込めるLoadString()メソッドも用意されています。