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

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

Effects10, Effects11の実行時コンパイル

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

以前書いた、「Effects11 for Windowsストア アプリ」の記事とは趣旨が逆行する話ですが、一応参考程度に書き残しておきます。デスクトップ アプリでは実行時コンパイルを使うこともできますが、いろいろ問題点(特にランタイム再配布に関するもの)があるので、極力実行時コンパイルは避けて、エフェクトやシェーダーのコンパイルには fxc.exe を使ってオフラインで行なうようにしておいたほうがいいです。

今回のサンプルは単純なコンソール アプリです。下記の環境で検証しています。
D3DEffectLoadTest.zip

昔(2010年頃)、Effects10を使っていたコードをEffects11に移行したときにハマったことがあったのですが、D3D10&D3DX10&Effects10と、D3D11&D3DX11&Effects11には細かい違いが結構あります。単純な置き換えでは済まない部分もあるし、紛らわしい部分もかなりあります。

Effects10 Effects11
対応するDirect3D API Direct3D 10.x Direct3D 11.x
使用可能なプロファイル名 fx_4_0, fx_4_1 fx_5_0
使用可能なテクニック technique10 technique10, technique11
外部ソースファイルからの直接生成 D3DX10CreateEffectFromFile() N/A
メモリ上ソースファイルからの直接生成 D3DX10CreateEffectFromMemory() N/A
ソースファイルのコンパイル D3DX10CompileFromFile() D3DX11CompileFromFile()
コンパイル済みバイトコードからの生成 D3D10CreateEffectFromMemory() D3DX11CreateEffectFromMemory()

特にまぎらわしいのが、下記2つです。

  • D3DX10CreateEffectFromMemory()
  • D3DX11CreateEffectFromMemory()

名前は似てますが、前者はメモリ上ソースファイルからのエフェクト生成、後者はメモリ上バイトコードからの生成であり、機能が全然違うので注意が必要です。

前者はFromSourceFileOnMemory()、後者はFromBytecodeOnMemory()にして欲しかった……

エフェクトのプロファイル名というのは、エフェクト ファイルをコンパイルするときに必要となるバージョンのようなもので、各シェーダーステージのプロファイル(vs_4_0やgs_5_0など)とは意味が異なります。fx_5_0を使っていても、vs_4_0_level_9_1などのダウンレベル プロファイルもきちんと使えます。

なお、D3DXはWindows SDK 8.0以降では廃止されていて、D3DCompilerランタイムもろともWindowsストア アプリ開発で使用できない(DLLをリンクするとストア アプリが起動できなくなる*1)ので、今後はできるかぎり使用を避けたほうが良いのですが、ディレクトリの優先順位を

Windows SDK 8.x>旧DirectX SDK June 2010

のように設定することで、Visual C++ 2012/2013コンパイラでもD3DXライブラリを補助的に使うことはできます。要するに、D3D9/D3D10.x/D3D11.xコア ライブラリはWindows SDK 8.xのものを使用して、D3DX9/D3DX10/D3DX11ライブラリは旧DXSDKのものを使うというわけです。これにより、デスクトップ アプリではD3D 11.1&D2D 1.1とD3DX11、もしくはD3D 11.2&D2D 1.2とD3DX11を共存させることも一応できます。

D3DX MathからDirectXMathへの移行は(演算子オーバーロードがなくなっていることを除いて)比較的簡単だと思います。DirectXTKのSimpleMathライブラリを使うとさらに簡単になるでしょう。D3DX TextureからDirectXTexへの移行も、互換機能がだいたい用意されているようなので、注意深く実装すれば大丈夫でしょう。しかし、D3DX Mesh/Sprite/EffectsなどからDirectXTKへの移行はかなり大変な作業になることが予想されます。代替機能がないものも多数あります。また、DirectXTKはD3D11専用で、D3D9/D3D10からは使用できません。実際にDirectXTex/DirectXTKへ移行するか、それともそれらに頼らずD3DXを自前のライブラリで置き換えるか、それを決めるまでの暫定的対処としてしばらくD3DXライブラリを使い続けるという手もあります。

ちなみにSMAAのソースに付属するDirect3D 10サンプルコードではEffects10を使っているのですが、FXLVM(Effects Language Virtual Machine?)が1か所だけ使用されています。Effects11はFXLVMをサポートしないので、移植しようとするとき変更が必要になります。

*1:D3DCompilerランタイムに関してはWindows 8.1ストア アプリにおいて制限付きながら利用できるようになりました。

Effects11 for Windowsストア アプリ

(これは2013-04-13に書いた故OCNブログの記事を加筆修正したものです)

今回は旧D3DXライブラリの歴史にちょっとだけ触れるので、前置きが長いです。

Windowsストア アプリを開発できるVisual Studio 2012用のDirectX SDKWindows SDK 8.0)では、これまでDirect3D開発者にとっておなじみだったD3DXライブラリがほぼ完全に廃止されています。D3DXのピークはDirect3D 9用のD3DX9で、数学関数などのルーチン系だけでなくフォントやスプライト、エフェクト フレームワーク(Effects9)、果てはスキンメッシュやアニメーション コントローラー、GI(PRT)用のSH計算関数までデフォルトで用意されている充実っぷりでした。時は流れ、プログラマブル シェーダーが必須となったDirect3D 10では、D3DX10が大幅に弱体化し、スキンメッシュやアニメーション コントローラーがなくなった代わりに、エフェクト フレームワーク(Effects10)がコアAPIに組み込まれました。しかし続くDirect3D 11ではD3DX11がさらに弱体化し、ついにエフェクトもビルド済みライブラリの形では提供されなくなり、スタティックライブラリ用のソースコード(Effects11)のみがDirectX SDKのサンプルとして付属するだけになりました。

デスクトップ アプリの開発では、Visual Studio 2012/2013において、Visual C++ 2010コンパイラWindows SDK 7.x+DirectX SDK June 2010などの組み合わせを引き続き使用するように設定することもできます(ただしWindows SDK 7.xを使用する場合、Windows SDK 8.0のDirectX 11.1やDirect2D 1.1は使えません。ツールセット設定やディレクトリ優先順位を調整して、D3DコアライブラリはWindows SDK 8.0/8.1を使い、DirectX SDK June 2010のD3DXのみ補助的に使う、ということはできます)。しかし、Windows 8/RT向けストア アプリの開発ではVS 2012とWindows SDK 8.0の組み合わせが必須となっており、またD3DXランタイムおよびD3DCompilerランタイムが一切使用できません(これらのランタイムをリンクしてしまうとアプリが起動不能になる)。HLSLの実行時コンパイルをストア アプリでサポートしない理由などは下記で言及されてます。

ちなみにNVIDIA C for Graphics(Cg)のランタイムは、実はToolkitバージョン3.1時点でD3DXとD3DCompilerのランタイムに依存しているため、Cgランタイムを使ったストア アプリは現時点では作れません。

代わりにストア アプリでのDirectX 11補助ライブラリとして、CodePlexDirectX Tool Kit(DirectXTK, DXTK)というのが公開されているのですが、こいつは(少なくとも自分にとっては)なんとも使いにくいライブラリになってしまっています。従来のエフェクト フレームワークでは、エフェクトファイル(.fxファイル)に複数のシェーダー関数やステートを記述して、組み合わせをpassとtechniqueの形で階層化しパイプラインを構築するということが簡単にできていたのですが、DXTKのEffectはC++コードでパイプラインをガリガリ組み立てていくという、なんとも原始的なことをやってます。最初から用意されている出来合いの基本シェーダーだけを使うのであればそれでもいいかもしれませんが、試行錯誤しながらパイプラインをガンガン書き直す場合は辛いです。なにより今までのエフェクトファイルの資産がほとんど使えず、エフェクトのサブセットフォーマットすらサポートされないので、パイプラインおよびシェーダーステージごとに分割していかなければならないのはかなりの苦痛。

そこで自分が考えたのは、

「Effects11はソース提供されてるんだから、ストア アプリで使えるように修正したらいいじゃない!!」

結論から言うと、わりと簡単にできます。ただしストア アプリ対応のためにD3DCompilerランタイムへの依存を断つといくつかの制限が発生するため、エフェクト フレームワークのリフレクション機能をフル活用しているようなプログラムに対してはコード修正が必要です。が、エフェクトファイルによるパイプラインの構築という最大のメリットがそのまま活かせるのは大きいでしょう。エフェクトファイルではcs_5_0のようなDirectX 11対応H/W用プロファイルだけでなく、vs_4_0_level_9_1やgs_4_0のようなダウンレベルH/W用のプロファイルも使えるので、Surface RT向けアプリ(ARM用アプリ)の開発にも使えるはずです。またD3DCompilerへの依存を断つことで、DirectXエンドユーザーランタイムをインストールする必要がなくなるため、従来のWindows 7向けデスクトップ アプリでこの制限付きEffects11を心置きなく使えるというメリットもあります。

%ProgramFiles(x86)%\Microsoft DirectX SDK (June 2010)\Documentation\License Agreements\DirectX SDK EULA.txt

を読むかぎり、Effects11に関しては改変・再頒布して良いらしいので、ストア アプリでも使えるように修正したCompact Effects11と、そのCompact Effects11を使ったストア アプリのサンプルをアップしておきます。利用は自己責任で。サンプルのほうはぶっちゃけVisual Studio 2012のDirect3DアプリのテンプレートをEffects11使って手直ししただけですが、ID3DX11EffectTechniqueは普通に使えることが実証されました。

なお、サンプルを見れば分かりますが、特に大きな制限として、

(1) D3DCompilerランタイムを使用した、エフェクトの実行時コンパイルができない
(2) 頂点シェーダーBLOBのリフレクションが効かない
(3) 名前によるエフェクト変数(シェーダー定数)の参照と更新はできない
(4) ステート構文によるSamplerStateの設定はできない
(5) 動的シェーダーリンク(Dynamic Shader Linkage)はローレベルAPIで対処

などがあります。

(1) はD3DX11CompileFromFile()が使えないということですが、コンパイルVisual Studioのビルドプロセスで行なって、実行時にコンパイル済みバイトコードをD3DX11CreateEffectFromMemory()に食わせてやることで対処します。VS 2012/2013であれば.fxファイルや.hlslファイルの単純なコンパイル設定はVisual Studio IDEに専用のツールが組み込まれているので簡単ですが(VC++のプロパティ設定UIが使える)、以前のバージョンのVisual Studioでデスクトップ アプリを開発する場合や、1つのファイルに対してオプションの異なる複数のコンパイル処理を走らせたりする場合はコマンドラインでfxc.exeを使って別途エフェクトファイルをコンパイルするか、カスタムビルドツール設定でのコマンドライン指定が要ります(※後述)。

(2) はID3DX11EffectPass::GetDesc()によるバイトコードの取得が使えないということですが、頂点シェーダーバイトコードのバイナリデータがないとID3D11Device::CreateInputLayout()による頂点入力レイアウトの作成ができません。エフェクトファイル本体のコンパイルとは別に頂点シェーダーだけエントリーポイント関数名を指定してコンパイルし、バイトコードを事前作成することで対処します。これも(1)同様です。

(3) はID3DX11EffectVectorVariable::SetFloatVector()などが一律使えないということですが、代わりにcbufferを使って、C++による明示的な定数バッファの作成と更新を行なうことで対処します。サンプラー変数やテクスチャなどのリソース変数(ID3DX11EffectSamplerVariable, ID3DX11EffectShaderResourceVariable)に関しても同様です。まぁヘタにエフェクト変数に頼りすぎるとDirect3Dデバッグレイヤーによるエラーレポートの解読が困難になるなどの問題をいろいろ引き起こすことがあるので、この際あきらめましょう。uniform変数ごとの名前アクセスが主流であるGLSLから移植したりする際はこのあたりに注意が必要です(OpenGL 3.1やES 3.0では逆に定数バッファ相当機能のUBOが追加されているので、cbufferとUBOを使えばDirect3D/OpenGL間でデータ構造の共有もしやすくなります)。

(4) はエフェクトファイルでなくC++でステートオブジェクトを作成・設定することで対処します。なお、サンプラーステートはC++で明示的作成する必要がありますが、レンダリングステートは従来通りエフェクトファイル側で書けます。

(5) はパフォーマンス損失を抑えつつシェーダーの組み合わせ爆発を防ぐために導入されたDirectX 11(シェーダーモデル5.0)の新機能で、シェーダープログラムでinterface(仮想メソッド)が使えるというとんでもない代物なのですが、(3)同様にID3DX11EffectInterfaceVariable::SetClassInstance()などはD3DCompilerのリフレクション(D3DReflect()関数)に依存しているため使えません。そのため、インスタンス テーブル用の定数バッファを明示的に宣言したり、ID3D11DeviceContext::XXSetShader()メソッドに対して明示的にID3D11ClassInstance配列を渡したり、といった工夫が必要になります(ちなみに動的シェーダーリンク目的以外でも、HLSLでclassキーワードを使って単純なクラスを定義することはできます)。ID3DX11Effect::GetClassLinkage()やID3D11ClassLinkage::GetClassInstance()に関しては普通に使えるようなので、Effects11を一切使わない場合と比べて多少は楽になる部分もありますが、ID3DX11EffectPass::Apply()はID3D11ClassInstanceを受け取れないのでやはりトリックが必要になります。実は、

// FX 側:
PixelShader g_psHyperShader = CompileShader(ps_5_0, psHyperShader());
// C++ 側:
m_pEffect->GetVariableByName("g_psHyperShader")->AsShader()->GetPixelShader(0, &pPixelShader);

という感じで特定のシェーダーステージのインターフェイスを取得する機能は動作するので、いったん動的シェーダーリンクを使わないダミーシェーダーを含んだエフェクトのパスを適用した後、特定のシェーダーステージだけ後から設定し直すということはできるんですが、複数のインターフェイスを使う場合は結局ID3D11ShaderReflection::GetNumInterfaceSlots()やID3D11ShaderReflectionVariable::GetInterfaceSlot()を使ってリフレクション経由でインターフェイス スロット情報を調べる必要があるため、動的シェーダーリンクを使うシェーダーだけはエフェクトとは別にコンパイルするようにしたほうが良いでしょう。

長々と書きましたが、D3DCompilerランタイムが使えないことによる制約はかなり大きいです。エフェクト フレームワークのほとんどの便利機能が使えなくなります。ただDXTKのEffectを使うよりは生産性が高くなることは間違いないし、これまでのエフェクトファイルの資産がある程度使えるのはありがたいです。

DirectX 12ではシェーダーステージの追加だけでなく、シェーダー自体の在り方もまたガラッと変わる可能性はあるので、Effects11を更新してEffects12に、ってわけにはいかないと思います。そもそも新しいD3DCompilerがリリースされても、DirectX 12対応のエフェクトファイル仕様(fx_6_0?, technique12など)はもう現れないでしょう。ただストア アプリの場合、DirectX 11.1/Direct2D 1.1や、DirectX 11.2/Direct2D 1.2テクノロジーにOSレベルでがっつり依存しているフレームワーク(&サンドボックス)内でしか動作できないため、DirectX 12がリリースされてもすぐに対応できるわけじゃないと思います(DirectX 12はXbox Oneにはプラットフォーム更新プログラムとして必ず供給されるでしょうが、Windows 8.xや7にバックポートされる可能性は低く、当然DirectX 12対応のストア アプリ/デスクトップ アプリはWindows 10でしか動作しないように思われます)。

ちなみにコンピュート シェーダー用のエクステンションであるD3DCSX拡張ライブラリは一応D3DXの仲間なのですが、例外的にWindows SDK 8.0/8.1にも同梱されています。ただし、これを使うとd3dcsx_46.dll/d3dcsx_47.dllに依存してしまうことに注意が必要。また、ストア アプリでは使えません。

カスタム ビルド ツールによるシェーダーコンパイル

カスタム ビルド ツールを指定するとき、例えば.fxファイルに対して下記のような「コマンド ライン」を指定します。

@echo off
setlocal EnableDelayedExpansion
echo Now compiling effect file by strict mode...
set varDefs=/D DOWN_SAMPLED_TEX_SIZE=128 /D COMPUTING_TEMP_WORK_SIZE=256
"%DXSDK_DIR%Utilities\bin\x86\fxc.exe" /nologo /Ges MyHLSL\MyShader.fx /Fo "$(OutDir)MyShader.fxbin" /T fx_5_0 %varDefs%
if "!ERRORLEVEL!"  neq "0" ( exit !ERRORLEVEL! )
"%DXSDK_DIR%Utilities\bin\x86\fxc.exe" /nologo /Ges MyHLSL\VSInOutStruct.hlsli /Fo "$(OutDir)MyVertexXX.vsbin" /E vsDummyLayoutXX /T vs_4_0_level_9_1
if "!ERRORLEVEL!"  neq "0" ( exit !ERRORLEVEL! )
endlocal

各オプションの意味は、fxc.exeのヘルプに書いてあります。

VS 2012/2013の場合は起動するだけで「%ProgramFiles(x86)%\Windows Kits\8.0\bin\x86」もしくは「%ProgramFiles(x86)%\Windows Kits\8.1\bin\x86」へのパスが通っているので、上記の「%DXSDK_DIR%Utilities\bin\x86\...」というパス指定は不要となり、「fxc.exe /nologo ...」でOKです(以前のバージョンのDirectX SDKのインストールも不要です)。

注意点としては、fxc.exeを実行するたびにERRORLEVELをちゃんとチェックすること、入出力に相対パスを指定するときはVC++プロジェクト ファイル(.vcproj)からの相対パスを記述すること。また、今回の例でいうと、「出力ファイル」に「$(OutDir)MyShader.fxbin」を、「追加の依存ファイル」に「MyHLSL\VSInOutStruct.hlsli;」を指定しておくと、ソースファイル.fxやヘッダーファイル.hlsliの変更、プライマリー出力ファイル.fxbinの不在を自動検出することができるようになります。

なお、「構成プロパティ」→「全般」→「クリーン時に削除する拡張子」に、*.fxbinや*.vsbinを指定しておくとさらに確実になるかもしれません。上記例の.fxbinや.vsbinは自分が勝手に付けたシェーダーバイトコード用拡張子です。VS 2012/2013の組み込みビルド ツールでは、シェーダーバイトコードの拡張子は.cso(Compiled Shader Object?)がデフォルトのようですが、あえて区別を付けるために分けています。

ちなみにVS 2012/2013でHLSLのシンタックス ハイライト(色分け)が効くようになる拡張子は、.fx, .hlsl, .hlsliだけのようで、「ツール」→「オプション」→「テキスト エディター」→「ファイル拡張子」で追加することもできない模様。

なお、自分はCg(.cg, .cgfx)、OpenCL(.cl)、GLSL(.vert, .frag, .glsl)のソースファイルをVisual Studioで編集する際はVC++エディタに関連付けていますが、float4やvec4などが色分けされないとちょっと不便。IDEの設定ファイルをいじると、ユーザー定義型をキーワードとして色分けすることもできるようになるんですが、わざわざそこまでするのも面倒……どのみちインテリセンスは効かないので、愛用のテキストエディタであるMIFESで編集したほうがよかったりします。

Windows 8.1のD3DCompiler

なおWindows 8.1DirectX 11.2では分割コンパイルのサポートに伴って、どうやらD3DCompilerのAPIの一部がストア アプリでもエンドユーザー環境で再び使えるようになっているらしいです。

復活した詳しい経緯は調べてませんが、たぶんDirectXのパワーユーザーである海外のハイエンドゲーム開発者から実行時コンパイルに関する強い要望もあったんだと思います(現にCrysisとかはシェーダーの実行時コンパイルを使って実行環境や設定に合わせて最適化してるっぽい)。個人的にはDynamic Shader Linkage用のリフレクション機能は便利だとは思うけど、それ以外は特に無くても困らないと思うんですが。また、HLSL shader compiler sampleなるものも公開されています。DirectX 11.2を採用しているXbox Oneでも同様にD3DCompilerが使えるようになる模様。まだ試してはいませんが、つまりWindows 8.1Xbox OneではVisual Studio 2013/Windows SDK 8.1に対応する最新のD3DCompilerランタイムが標準インストールされていて、デスクトップ アプリ同様にDynamic Shader LinkageはもちろんEffects11の全機能が使えるってことでしょう。ただ、Win7Win8.0などの旧環境でも(D3DCompilerランタイムの追加インストールをすることなく)そのまま動作するようにしたければ、今回紹介したCompact Effects11を使う意義はありそうです。

オリジナルのEffects11に関して

今回ベースにしたEffects11は、DirectX SDK June 2010に付属していたソースコード+プロジェクトです。なお、Effects11自体は一応CodePlexに移管されているようです。ただしD3DCompilerに依存する従来のコードをそのまま使っているらしく、Win8.0ストアアプリには使えない模様(ストアに提出できない)。いずれにせよ、将来的にDirectX 12のシェーダーモデル6.0対応fxc.exeではEffects12やエフェクトファイルの新しいプロファイル(fx_6_0?)やテクニック(technique12)が提供されない可能性が高いので、それを覚悟して使う必要があります。

Visual C++ 2010 SP1でのMFC拡張

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

Microsoftが.NETに注力を始めてからずいぶんと影が薄くなったのが、Visual C++向けのWin32 APIラッパー/デスクトップアプリケーションフレームワークであるMFCなんですが、VC 2010になっても結局MFCはひっそりと拡張され続けています。2010ではリボン リソース エディター(リボン デザイナー)などの目玉機能が追加された挙げ句、SP1では下記2つのコア機能が追加になっています。

あまり話題にはなっていないけど、WPFをどうしても採用できない案件で、高速かつリッチなUIをネイティブ実装するのに使えそうです。どのみちコードベースでガリガリ書いていく必要があるので、生産性や保守性の観点からはWPFには遠く及ばないんですが、D2D/WAMはCOMゆえに結構面倒な部分があるので(COMを知らない人には使えない、また知っていてもコールバック*1まわりが面倒)、そのラッパーが用意されているのは助かるかもしれません。

D2Dが使えるのはWindows 7/Vista SP2 + Platform Update以降であり、Windows XPではD2Dがサポートされないので当然ハードウェア アクセラレーションが効かないのはともかくとして、多分WAMラッパーのほうもXPでは機能制限があるようです。というかD2D/WAMはXPにはバックポートされていないので多分ラッパーも使えません。
WPFは豊富なアニメーション機能が便利なんですが、そのフレームワークのみをネイティブCOM実装したのがWAMになります。とはいえXAML連携などはできません。

Visual Studio 2010 SP1 用の MFC の追加
https://msdn.microsoft.com/ja-jp/library/gg482719.aspx

チュートリアル: MFC プロジェクトへの D2D オブジェクトの追加
https://msdn.microsoft.com/ja-jp/library/gg482848.aspx

チュートリアル: MFC プロジェクトへのアニメーションの追加
https://msdn.microsoft.com/ja-jp/library/gg466500.aspx

VS 2010 SP1をインストールすると、下記の場所にサンプル コードのアーカイブがインストールされます。ただし中に入っているのは差分サンプルが少しだけです。
%ProgramFiles(x86)%\Microsoft Visual Studio 10.0\Samples\1041\VC2010SP1Samples.zip

MFCソースコードが付属しているし、マニュアルもそれなりにあるので、ヘビーユーザーであれば使い方は自分で調べればよいのですが、もっと有用な実装サンプルが増えないと、初心者には厳しいかもしれません。
また、MFC用に提供されているDirect2Dラッパーは、Direct2D 1.0専用になります。Windows 8に実装されているDirect2D 1.1や、Windows 8.1に実装されているDirect2D 1.2を使いたい場合は、MFCラッパーではなく直接COM APIを操作する必要があります。


しかし結局COMはいつまでも残り続けるしぶとい技術ですね。DCOMはWCFなどによってほぼ完全に置換されましたが、COMはネイティブC++から利用できるソフトウェアコンポーネント再利用技術として、今後もしばらくMSプラットフォームの中核であり続けるでしょう。実はWindows 8.xのWinRTもCOMの拡張技術だったりします。

*1:COMのコールバックはイベントシンクという仕組みを使います。イベントシンクはプロセス透過でDCOMにも使える反面、登録作業がかなり煩雑です。

GDI+でアンチエイリアスEMFを作成

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

MFCCMetaFileDCWindows拡張メタファイル(EMF)を作成した場合、たとえGdiplus::Graphics::FromHDC()でバインドしたGDI+グラフィックスを使ってそのメタファイルを描画してもアンチエイリアスがかかりません。

アンチエイリアスのかかったWindows拡張メタファイル(EMF+)を作りたい場合、Gdiplus::Metafileを使う必要があります。Gdiplus::EmfTypeEmfPlusOnlyもしくはGdiplus::EmfTypeEmfPlusDualオプションを付けてストリーム上に作成したMetafileに、Graphics::FromImage()でバインドしたGDI+グラフィックスを使って描画すればOK。Metafileコンストラクタの第1引数にはファイルパス文字列のほかにCOMのIStreamを渡せるオーバーロードがあるので、ファイルストリーム上だけでなくメモリーストリーム上にメタファイルを作成することもできます。

なお、Graphics::EnumerateMetafile()を使うことで、アンチエイリアスのかかっていないEMFファイルを開き、アンチエイリアスのかかったEMF+ファイルに変換して書き出す、ということもできます。

「EnumerateMetafile()を使ってEMFをEMF+に変換するにはどうすればいいの?」という質問をいただいたので、参考までに簡単なサンプルを下記にアップロードしておきました。ただし、色の半透明化として使われているタイル パターン ブラシまわりなど、対応が不十分だったり、完全に未対応だったりするEMRコマンドがたくさんあります。ソースコードの2次利用はご自由に、ただし自己責任でどうぞ。

MetafileConvertTest.zip

なお、EnumerateMetafile()で列挙されるレコード(コマンド+付随データ)をそのままMetafile::PlayRecord()に渡しても、アンチエイリアスの効いたEMF+(GDI+コマンド)に自動変換してくれるようなことはありません。EMRレコードの意味的な解析が必要になります。また、EMRコマンド構造体が必要になるので、.NETよりネイティブC++のほうが変換処理を書きやすいはずです。変換プログラムとしては、コマンドラインのフリーウェア「EMF to EMF+ Converter」がありますが、こちらはソースコードが公開されてないようです。ちなみにGDIのコマンドと従来のEMFファイルのコマンドは1対1で対応するため、EMFファイルからGDIコマンドを復元するのは容易ですが、GDI+のDrawRectangle()やDrawEllipse()で描画した図形を、従来のEMF Onlyメタファイル形式で保存すると、PolygonやBezierで記録されてしまいます(1対1で対応しない、つまり情報が欠落してしまう不可逆変換)。なので、EMF OnlyのGDIコマンドを、GDI+のDrawPolygon()やDrawBeziers()で変換すること自体はできますが、元となる描画に使用されたGDI+コマンドを復元するのは困難となります。個人的には、EMFからEMF+に変換するよりは、メタファイル生成プログラムを書き直して最初からEMF+として出力するように修正することをお勧めします。

■ EMF+ (オリジナル)
https://sygh-jp.github.io/content_hosting/my_program_ss/emf_plus_test.png

■ EMF (ダウンコンバート、アンチエイリアスの欠落と半透明効果のタイル化)
https://sygh-jp.github.io/content_hosting/my_program_ss/emf_only_test.png

■ EMF+ (アップコンバート、ただし半透明効果の復元には未対応)
https://sygh-jp.github.io/content_hosting/my_program_ss/emf_plus_convert_output_test.png

ネイティブXPSドキュメントAPIが使えるWindows 7(とVista SP2 + Platform Update)以降では、メタファイルの存在意義が小さくなりつつありますし、WPFではWindowsメタファイル形式がそもそもサポートされていません。が、WordやExcelに直接貼り付けられるEMFは、Windows上でのベクトル画像フォーマットとしてまだ利用価値がありそうです。

ちなみにWin7からVistaにPlatform Updateの形でバックポートされたネイティブAPIは、XPSのほかにもかなりたくさんあります。自分が知っている範囲でも、リボン フレームワーク、Direct2D/DirectWrite、DirectX 11、Windows Animation Manager (WAM)、など。全部COMです。WAMはゲーム開発にも使えそうです。

Windows 8(とWin7 SP1 + Platform Update)以降に搭載されている新しいDirect2D 1.1は、印刷処理やメタファイルへの出力もサポートするらしいんですが、メタファイルってのはどういうフォーマットになるんでしょうか? EMF+をサポートするわけじゃないと思うけど……やっぱりXPSなんでしょうか? いずれ試してみたいと思います。

Windows SDKと旧DirectX SDK

DirectX SDKは、DirectX 11.0対応の「June 2010」がリリースされた後、まったく新しいバージョンが出ない時期が続いたんですが、Visual Studio 2012にバンドルされているWindows SDK 8.0において、DirectX SDKWindows SDKと統合されました。

昔(DirectX 9や10.xの時代)はかなり頻繁に、それこそ2、3ヶ月おきぐらいにDirectX SDKが更新されていたんですが、今後はDirectX APIの更新はWindows SDKの更新と同程度の頻度になるようです(つまりWindowsのバージョンアップと同時期にDirectXもアップデートされる)。また、旧DirectX SDKにはDXUTフレームワークとともに大量のハイレベルかつ有用なサンプルコードが付属していたんですが、DXUTやD3DXの一部はGitHub*1に移管され、またサンプルコードはVisual Studio 2012向けに移植された一部のサンプルがDeveloper Center (Developer Network) で個別に公開されています。

ちなみに旧サンプルのうち、AMDの開発者が実装に関わったと思われるもの(ContactHardeningShadows11など)に関しては、AMD Radeon SDK (ATI Radeon SDK) に移管されているそうです*2

Visual Studio 2012付属のWindows SDK 8.0で使用可能なDirectXコンポーネントは下記です(太字は新規)。

  • Direct3D 9, 9Ex
  • DXGI 1.0, 1.1, 1.2
  • Direct3D 10.0, 10.1
  • Direct3D 11.0, 11.1
  • Direct2D 1.0, 1.1
  • DirectWrite 1.0, 1.1
  • DirectSound
  • XAudio2 (v2.8) *3
  • X3DAudio
  • DirectInput
  • XInput (v9.1.0, v1.4) *4
  • DirectShow
  • DirectDraw

Windows 8標準搭載のDirect2D 1.1では、シェーダーで実装された定義済みエフェクトを使用できるほか、プログラマーが頂点シェーダー、ピクセル シェーダー、コンピュート シェーダーにアクセスできるようになっています。
また、ブレンドモード(Composite Mode)を制御することで、加算合成などを容易に実現できるようになっています。
APIDirect3D 11.1上に再構築され、Windows Vista/7向けのDirect2D 1.0(Direct3D 10.1ベース)と比べてDirect3D APIへのアクセスや連携が簡単になっています。

Visual Studio 2013付属のWindows SDK 8.1で使用可能なDirectXコンポーネントは下記です(太字は新規)。

  • Direct3D 9, 9Ex
  • DXGI 1.0, 1.1, 1.2, 1.3
  • Direct3D 10.0, 10.1
  • Direct3D 11.0, 11.1, 11.2
  • Direct2D 1.0, 1.1, 1.2
  • DirectWrite 1.0, 1.1, 1.2
  • DirectSound
  • XAudio2 (v2.8)
  • X3DAudio
  • DirectInput
  • XInput (v9.1.0, v1.4)
  • DirectShow
  • DirectDraw

デスクトップ アプリでは当然すべてのDirectXコンポーネントを使用できますが、Windowsストア アプリ開発に使用できるDirectXコンポーネントは、Direct3D 11/Direct2D/DirectWrite、XAudio2/X3DAudio、XInputなど、列挙した中の一部のみとなります。後述のD3DCSXとD3DCompilerを除いて、Windows SDK 8.0に含まれるDirectXコンポーネントはすべてWindows 8以降に標準インストールされており、またWindows SDK 8.1に含まれるDirectXコンポーネントはすべてWindows 8.1以降に標準インストールされているものなので、上述のコアコンポーネントのみを使って開発しているかぎり、アプリケーションの実行に際してDirectXエンドユーザーランタイムなどを別途インストールする必要はありません。

なお、これは予想ですが、2015年にリリースされる予定のVisual Studio 14正式版では、3月のGDC 2014にて発表されたDirectX 12 (Direct3D 12) と、9月のNVIDIA Editor's Dayにて先日発表されたDirectX 11.3 (Direct3D 11.3) が使えるようになるものと思われます。デスクトップアプリではもちろん11.3も12も両方使えるはずですが、ストアアプリではどうなるんでしょうか……
本記事の執筆時点ではDirectX Early Access Programに参加しているユーザーのみが、DirectX 12ベータをテストすることができます。

D3DX/DXUTの廃止

Windows SDK 8.0に統合された新しいDirectX SDKでは、D3DXやDXUTが廃止されています。D3DXは非常に便利な公式ライブラリで、DirectX 9全盛期にD3DXを使ったことがないという個人開発者はおそらくいないのではないでしょうか。Crysisのように有名なAAAタイトルのPCゲームでもD3DXが使われていました。

下記にD3DXからのポーティングに関して比較一覧があるようです。

DirectXMath

D3DXのうち、算術ライブラリに関しては代替として、Windows SDKにDirectXMathという算術ライブラリが標準で用意されるようになります。

DirectXMathは、もともとWindows/Xbox両対応のクロスプラットフォームな算術ライブラリとして開発されていたXNA Mathの後継ライブラリです*5。内部的にはXNA Math 3.xの扱いになっています。ただし完全なスーパーセットというわけではなく、ARM対応などもあって一部互換性がないのですが、ほとんど同じ要領で使えます。

とりあえず、今D3DX Mathを使っているけどDirectXMathが使える環境ではないという人は、早めに単独の最終バージョンである XNA Math 2.05 を試すなりして慣れておいたほうが良いでしょう。

DirectXTK, DirectXTex, DirectXMesh

D3DXのうち、スプライト、フォント、メッシュ、エフェクト、テクスチャなどのユーティリティ系は、DirectX Tool Kit (DirectXTK, DXTK), DirectX Texture Processing Library (DirectXTex), DirectXMesh geometry processing library (DirectXMesh) というオープンソースのライブラリにそれぞれ分割・移行されました。

GitHub:

CodePlex:

Direct3D 11で手軽に使えるスプライトやフォントのライブラリができるのはいいことだと思うんですが、DXTKのフォントライブラリはデフォルトで日本語などの非ASCII文字には対応していません。また、フォントテクスチャ画像はGDI+.NETを利用して事前作成する仕組みなので、DirectWriteを使う場合と比べて描画品質が劣ります。もし単にDirect3D 11.1以降の環境で文字列描画を行ないたいだけであり、また限界性能を追及しないのであれば、Direct2D/DirectWriteを使うのが無難です。
なお、DXTKのエフェクトは、フレームワークとしては非常に貧弱です。.fxファイルを扱うこともできません。
メッシュは3Dプログラミングに慣れている人であればすでにDirect3D 9/10/11およびOpenGL両対応のライブラリなりを自作していると思いますが、DirectXTK, DirectXTex, DirectXMeshはいずれもオープンソースなので、パワーユーザーであっても参考になるコードがあるかもしれません。ちなみにDirectXMeshはかつてD3DXに実装されていたようなメッシュ最適化機能を備えているようです。

DXUT, Effects

DXUTフレームワーク、およびEffects 11フレームワークは、前述のようにGitHubに移管されていますが、互換性のために一応残っているというレベルなので、特にEffects 11に関しては今後積極的に使うべきではないと思います。
Visual Studio 2012/2013には、WindowsストアアプリのDirect3D/Direct2Dプロジェクト テンプレートは存在するんですが、Desktopアプリ用のDirect3D/Direct2Dプロジェクト テンプレートはありません。技術論文の検証プログラムや、NVIDIAIntelAMDが公開してるサンプルは結構DXUTを使っているものが多いのですが、今後(特にDirectX 12以後)はどうなるんでしょうか。DXUTはWin32/MFC/WinForms/WPFと違ってフルスクリーンモードでもUI部品が使える(部品は単なるテクスチャなので、Direct3Dレンダリングバッファにそのまま合成できるし、IMEにも対応している)のが強みで、Windowsおよびコンソールにおけるクロスプラットフォームなゲーム用ユーザーインターフェイス実装の良いサンプルという役目も果たしていました。Windows 8.x以降やXbox One限定であればWindowsストアアプリというサンドボックスが代わりになる……かもしれません*6

D3DCSX

D3DXのうち、DirectCompute (Compute Shader) のユーティリティであるD3DCSXだけはWindows SDK 8.0以降にも残っています。ですがD3DCSXライブラリはWindowsストアアプリでは使えないので注意しましょう。
もしWindowsストアアプリでGPGPUをやりたいだけであれば、DirectComputeを直接使う以外にC++ AMPを利用するという手もあります。

Windows SDK 8.0にはD3DCSX_46.dllが、Windows SDK 8.1にはD3DCSX_47.dllがそれぞれ同梱されていますが、いずれもWindowsに標準インストールされていないため、開発者がアプリケーションに同梱する必要があります。

D3DCompiler

D3DCompilerはアプリケーション実行時にHLSLシェーダープログラムをソースコードからコンパイルしてシェーダーバイナリを生成したり、リフレクションを利用するときに使うライブラリです。前述のEffectsの一部機能はリフレクションに依存しているため、D3DCompilerが必要となります。D3DCompilerはWindows 8向けのストアアプリでは利用できませんでしたが、Windows 8.1向けのストアアプリでは利用できるようになりました。
Windows SDK 8.0にはD3DCompiler_46.dllが、Windows SDK 8.1にはD3DCompiler_47.dllがそれぞれ同梱されていますが、D3DCompiler_46.dllはWindowsに標準インストールされていません。D3DCompiler_47.dllはWindows 8.1に標準インストールされていますが、Windows 8以前には標準インストールされていません。これらをデスクトップアプリで利用する場合は、通例D3DCSX同様に開発者がアプリケーションに同梱する必要があります。ただ、OpenGLと違い、DirectXではシェーダーをオフラインコンパイルすることができるので、特に理由がないかぎりアプリケーションはD3DCompilerに依存しないように設計するほうが好ましいです。

dxerr

dxerrはDirectX APIが返すHRESULTエラーコードから対応する識別子を文字列として取得したり、デバッグビルド時にトレース出力したりする関数を含むユーティリティです。このライブラリは他のDirectX APIと違ってDLL実装ではなく、スタティックリンクライブラリによる実装でした。DirectX SDK June 2010を最後に廃止され、Windows SDK 8.0以降には含まれていませんが、代替のヘッダーとソースが以下にて公開されています。

D3DX MathからXNA Math (DirectX Math) への移行

今後はD3DXが廃止されてDirectXMath, DirectXTK, DirectXTex, DirectXMeshなどに移管されますが、XNA Math/DirectXMathはかなりローレベルのAPIです。雰囲気的にはSSEのintrinsicと大差ありません。
D3DXの便利な機能(特にD3DXVECTOR2/D3DXVECTOR3/D3DXVECTOR4/D3DXMATRIXの演算子オーバーロード)がXNA Math/DirectXMathには存在しないどころか、多くの算術演算関数はXMVECTOR型(メンバーにアクセスすることは許されない、不透明な構造体。SSEを有効にすると、_m128型になる。主に中間計算の一時変数用途)やXMMATRIX型を引数にとるようになっているので、簡単にD3DX型とD3DX関数だけを置き換えるというわけにはいきません。XMFLOAT2/XMFLOA3/XMFLOAT4/XMFLOAT4X4をストレージ(クラスのメンバー変数など)に使う場合、

XMVectorGet*()
XMVectorSet*()
XMLoad*()
XMStore*()

を使ってXMVECTORから成分取得、あるいはXMVECTORへ成分設定してやる(相互変換してやる)必要があります。つまりSSEの知識(アライメントされたデータを専用レジスターへロード/ストアーする処理)がある程度必要になります。
こりゃD3DXのほうが圧倒的に楽だな……と思うかもしれませんが、XMLoad*(), XMStore*()を内部で使って、D3DXとほぼ同じ仕様のI/Fを持つラッパー関数を書けばいいだけなので、移行自体はそんなに大変ではないと思います。

例えばD3DXの、

D3DXMatrixTranspose(&outMatrix, D3DXMatrixLookAtRH(&outMatrix, &vEye, &vAt, &vUp));

に相当するXNA Math/DirectXMath実装は以下のような感じ。

inline void CreateTrMatrixLookAtRH(XMFLOAT4X4* pOut, const XMFLOAT3* pCameraEye, const XMFLOAT3* pCameraAt, const XMFLOAT3* pCameraUp)
{
    const XMVECTOR vCameraEye = XMLoadFloat3(pCameraEye);
    const XMVECTOR vCameraAt = XMLoadFloat3(pCameraAt);
    const XMVECTOR vCameraUp = XMLoadFloat3(pCameraUp);
    const XMMATRIX mView = XMMatrixLookAtRH(vCameraEye, vCameraAt, vCameraUp);
    XMStoreFloat4x4(pOut, XMMatrixTranspose(mView));
}

ぱっと見D3DXバージョンよりも呼び出す関数の数が増えて遅くなるんじゃないかと思えますが、XNA Mathの関数はいずれもインライン実装されてるし、Load/Store系は_m128型変数への転送を行なっているだけなので、リリースビルドでは多分ほとんど変わらないかXNA Mathのほうが高速になる可能性もあります。ベンチマークは取ってませんが。

なお、上記サンプルコードではconst XMVECTORとかconst XMMATRIXとかいう風に、XNA Math関数の戻り値をローカル変数に一時的に格納する際に型を明示していますが、ここでC++11の型推論(auto)を使ってしまうと、リリースビルドではVisual C++コンパイラーが不正な最適化を行なってランタイムエラーになることがあるみたいなので注意。また、XMVECTORやXMMATRIXは16バイト境界でアライメントされたメモリーブロックを必要とするため、基本的にヒープ上に作成してはダメで、通常はスタック変数として確保する必要があります(つまり基本的にはローカル変数や関数パラメータとしてのみ確保する)。newなどでヒープされる可能性がある構造体・クラスのメンバーにこれらの型は使わないほうがよいです。どうしてもヒープする必要がある場合はアライメント対応の専用ヒープ関数を使う必要がありますが、面倒なのでまずやらないほうがいいでしょう。普段のデータ保持目的や、定数バッファ経由でGPUに渡す際に使うのは、扱いやすいXMFLOAT2/XMFLOAT3/XMFLOAT4/XMFLOAT4X4にしておきましょう。

また、DirectXTKには、D3DXのD3DXVECTOR2/D3DXVECTOR3/D3DXVECTOR4/D3DXMATRIXのように演算子オーバーロードが定義されたC++向けのユーティリティクラス群を含むSimpleMathが用意されています。こちらを利用するとD3DX Mathから移行しやすいでしょう。

ちなみにXNA Math/DirectXMathでも相変わらず倍精度浮動小数(double)のサポートはないです。仕事で倍精度対応のD3DXライク算術ライブラリを書いたことがありますが、結構面倒です。GPGPUで倍精度演算を今後使うことがある人は、GLM (OpenGL Mathematics) のdvecを使うといいかもしれません。Intel AVX命令が使える場合はdvec4もdvec2並に高速化されると思います。

なお、DirectXTKやDirectXTexは既定でDirectXMathを要求するのですが、DirectXTexに限っては USE_XNAMATH というシンボルを定義することで、DirectXMathの代わりにXNA Math 2.05を使うことができるようになるらしいです。ただ、DirectXTKもDirectXTexも(一部を除いて)Direct3D 11.xにしか対応していないので、Direct2DやDirectWriteと組み合わせるならばVisual Studio 2012+Windows 8あるいはVisual Studio 2013+Windows 8.1の環境がベストかと。

その他の非推奨コンポーネント

DirectMusicやXACT (XACT3) などは、旧DirectX SDKの特定バージョンを最後に、ヘッダーやライブラリが収録されなくなっています。Windows OS自体は、互換性維持のためにまだこれらの機能(コンポーネント)を搭載しており、例えばWindows 8.1でも一応使うことはできますが、新しいSDKでは開発がサポートされていない非推奨のAPIになっています。中には64bitネイティブコンポーネントすら用意されていないものもあります*7Windows SDK 8.xに収録されていないということは、当然Windowsストアアプリ開発には使えません。このあたりの経緯や詳細に関してはMSのChuck Walbourn氏による下記の公式ブログが詳しいです。

ちなみに過去の頻繁なSDKアップデートでどういった機能が追加されていたのか、詳細を知りたい場合は下記が参考になります。

*1:最初はCodePlexに移管されていましたが、のちにGitHubに移管されました。さらにCodePlexは2017年4月に年内閉鎖がアナウンスされました。

*2:2017年5月現在、ともにリンク切れとなっています。AMDは責任感がないですね。どうしても必要な人は旧DirectX SDK June 2010をダウンロードしてください。

*3:Windows XP/Vista/7でも使えるXAudio2はv2.7ですが、v2.7を使うには旧DirectX SDK June 2010を使って開発する必要があり、また実行にはDirectXエンドユーザーランタイムをインストールする必要があります。

*4:XInput Versions - Win32 apps | Microsoft Learn

*5:XNA Math自体は、XNA Game Studio/XNA Frameworkとは直接の関係はありません。

*6:Windowsストアアプリのフルスクリーンモードは、従来のビデオメモリを占有する排他的フルスクリーンモードではなく、単にウィンドウ領域を画面いっぱいに広げただけの疑似フルスクリーンです。実質ウィンドウモードなので、パフォーマンスは排他的フルスクリーンモードに劣ります。

*7:64-bit programming for Game Developers - Win32 apps | Microsoft Learn, ゲーム開発者のための 64 ビット プログラミング | DirectX SDK (August 2009) | MSDN | Internet Archive

C/C++可変個引数のダークサイド

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

C/C++ の可変引数(可変個引数、可変長引数)はとても強力です。こいつにお世話にならない人はおそらくほとんど居ないと言っても過言ではありません。とくに printf/scanf 系の文字列入出力関数は、C++ のストリームが使いづらい*1せいで、たとえ C++ のヘビーユーザーであってもずっと使い続けている人も多いことでしょう。かくいう自分もその一人です。C++ のストリームは型安全なのが良いところなんですが、細かい書式を指定しようとすると、記述があまりにも冗長になりすぎます。

ただ、可変個引数は値をパラメータとして渡した時点で、一旦パラメータの型情報が失われてしまうせいで、型安全なプログラムを記述することができないという致命的な欠点があります。具体的にどんなのがやばいかというと、例えば Windows の ATL/MFC プログラムでこんな記述をするときでしょうか。

const char* pParam = "Hello, World!";
CString str;
str.Format("%s\n", pParam);

このコード、MBCS(ANSIマルチバイト文字セット)設定では普通にコンパイルが通るし、正しい実行結果が得られるんですが、Unicode 設定ではコンパイル エラーになってしまいます。CString が内部で管理しているバッファは、正確には char 型配列でなく TCHAR 型配列であり、また CString::Format() 関数の第一引数の型 LPCTSTR は const char* 型でなく const TCHAR* 型です。
それゆえ、プロジェクト設定に応じて、UNICODEシンボルが定義されていない場合にはそれぞれ char 型配列、const char* 型ですが、一方UNICODEシンボルが定義されている場合にはそれぞれ wchar_t 型配列、const wchar_t* 型になるからです。

UNICODE シンボル定義有無と各種データ型の変化

UNICODE シンボル未定義 UNICODE シンボル定義済み
TCHAR char wchar_t
LPTSTR char* あるいは LPSTR wchar_t* あるいは LPWSTR
LPCTSTR const char* あるいは LPCSTR const wchar_t* あるいは LPCWSTR
CString CStringA CStringW
_T("hoge"), TEXT("hoge") "hoge" L"hoge"


しかたねーな、じゃあこれでどうよ?

str.Format(_T("%s\n"), pParam);

確かにこれだと MBCS だろうが Unicode だろうがコンパイルは通ります。<tchar.h> で定義されている _T() マクロは Unicode 設定のときにリテラル文字列をワイド文字列化してくれる仕組みになっているからです。しかし pParam の型は相変わらず const char* なので、Unicode 設定では正しくデータを解釈できず、実行時に文字化けしてしまいます。CString::Format() 関数は、%s 書式に対応する文字ポインタ型として、const TCHAR* を想定しているからです。つまり正しくは、上記修正に加えてさらに、

const TCHAR* pParam = _T("Hello, World!");

あるいは

str.Format(_T("%s\n"), CString(pParam).GetString());
// キャスト演算子オーバーロードを使って以下のように書くこともできる。
str.Format(_T("%s\n"), static_cast<LPCTSTR>(CString(pParam)));

と修正することも必要になります。後者はリテラル以外にも対応できますが、実行時に const char* から CString へ変換するためのオーバーヘッドが加わるので注意。CString 一時変数の代わりに「ATL と MFC の文字列変換マクロ」を使う方法もあります。

ここで何が問題かというと、CString のバッファが char 型であることを想定した既存の古いコード(特に最初 VC++ 7.1 以前の処理系向けに書かれていたがために、VC++ 8.0 以降の新しいコンパイラを使いながらも未だに MBCS バージョンのライブラリを使っているコード)を Unicode 移行するためにコードを修正するときに、コンパイラがエラーを出力してくれている箇所以外にも目を光らせないといけない、ということ。型安全性が担保されない可変個引数を誤って使うと、「コンパイラが検出できない(コンパイル時には分からない)コーディングミスによるバグ」を作り込んでしまうことになります。

ちなみに ATL/MFC の CString::Format() の他にも、Win32 API の wsprintf() 関数が可変個引数を用いています。wsprintf() はUNICODEシンボル定義の有無で wsprintfA() もしくは wsprintfW() のいずれかに展開されます。標準Cライブラリの swprintf() とは名前が似ていますが出自も機能も別物の関数なので注意。なお、sprintf_s(), swprintf_s() と比較して、wsprintf() はいろいろと制限や問題を抱えているので、よほどのことがない限り使わないほうがいいでしょう。

コンパイル時にはエラーとならず、実行するまでエラーが発覚しない、というのは最も厄介です。なぜなら全てのフローや条件をテストしないとバグが発覚しないということの裏返しでもあるから。バカ正直にテストしようとすると組み合わせ爆発で破たんするのは目に見えています。テストを自動化しているコードならばともかく、Unicode 関連の修正の影響は自動化の難しい UI まわりに如実に表れるので、余計にタチが悪いです。さらに、書式のミスによりランタイムエラーや例外が送出されるならばともかく、実際は目に見える形で即エラーになることはありません。せいぜい表示がおかしいとか、出力結果にゴミが混ざるとかいった程度で、いったい何が原因で引き起こされた不具合なのかをすぐに特定するのは難しいでしょう。場合によってはホワイトボックステストが必要になります。単体テストをきっちりやらずに、いきなり結合してビッグバンテスト、なんてプロジェクトだとなおさらです。エンドユーザー運用環境で初めてバグが発覚するという最悪のケースもありえます。

WinAPI/ATL/MFC 初心者は、まず間違いなく TCHAR 関連の話が分かっていないというかそもそもワイド文字とか MBCS とか Unicode とか何それ食えんの状態であること間違いなしなので、このことを教えないで初心者に大量の Windows アプリケーションコードを組ませるべきではありません。特に素人に printf 系の書式と可変個引数を使わせると、後で修正&デバッグするのに多大な労力が必要となります。まぁそもそも初心者に十分な教育を施す前に、いきなり製品用プログラムのソースを触らせる時点で間違っているわけですが……

もしあなたが新人を指導する立場にあり、さらにあなたが関わっているソフトウェア製品が(日本語を含む)多言語環境向けに開発されているのであれば、文字および文字列に関する(正しい)知識は最初から徹底的にたたき込むことを強く推奨します。将来的に使い物にならないゴミコードが大量生産されていく前に……


なお、C/C++ は多バイト文字列どころか、文字および文字列自体のサポートがあまりに弱いので、昔からこのあたりは苦労していたところですが、C++11/C11 (C++0x/C1x) ではようやく UTF-16UTF-32 向けの文字型 (char16_t, char32_t) が用意されることになります*2。個人的には、int8_t / uint8_t 型を組み込み型に昇格させて、(char 型に依存しない形で)言語標準レベルで1バイト整数型をサポートして欲しいんですが。これは int32_t / uint32_t 型にも言えます。でないとオーバーロードがらみの問題が発生して見苦しい上にややこしいです。sizeof(int) とか sizeof(long) とか sizeof(long long) とかあまつさえ sizeof(wchar_t) までもが処理系依存*3とかいう C/C++ のヘボ仕様は、もういい加減勘弁してほしいです……JavaC# がはるか昔に(というか最初から)サポートしているものを、いつまでたってもサポートしようとしない C/C++ にはホント嫌気が差してきます。膨大なC/C++コード資産を再利用しなければならないなどの正当な理由がないかぎり、今後はJava/C#のようなプログラマーに優しい真の高水準言語を積極的に選択するべきだと思います。

余談:%lc, %ls, %wc, %ws, %C, %S 書式

%c, %s には長さ修飾子 l を指定することができます。本来、wchar_t型やwchar_t*型を可変個引数に指定する際には必須の修飾子なのですが、MSVC付属の CRT およびそれらを内部的に使用している ATL/MFC では、ワイド文字版関数に関しては %lc, %ls はそれぞれ %c, %s のシノニムになっており、長さ修飾子を指定せずともwchar_t型やwchar_t*型とみなすようになっています(標準規格外の動作)。なお、非標準の長さ修飾子として wもサポートしており、%wc, %ws はそれぞれ %lc, %ls と等価です。

MSVCでは、ほかに非標準の書式として %C, %S 書式もサポートしています。これらの書式を使うと、マルチバイト書式文字列でワイド文字列を可変個引数に受け付けたり、逆にワイド書式文字列でマルチバイト文字列を受け付けたりできます。具体的には、

CStringA strA;
strA.Format("%S\n", L"hoge");

とか、

CStringW strW;
strW.Format(L"%S\n", "hoge");

とかいうことができるようになります。ただし、CString の場合、const TCHAR* 書式文字列において %C, %S を使うと、MBCS 設定と Unicode 設定では意味が逆になり混乱するので、使用は推奨されません。また、内部的に char ⇔ whcar_t の変換が入るので、統一できるときはしておかないと余計なオーバーヘッドが発生する羽目になります。どちらにせよ、やはり可変個引数は MBCS と Unicode をきちんと理解している人間のみが使うべきでしょう。

なお、size_t と ptrdiff_t に関しても、サイズがターゲット プラットフォームに依存するので、こいつらを文字列変換する場合にも細心の注意が必要となります。

sygh.hatenadiary.jp

※2021-03-14追記:
C++20ではstd::format()が追加されました。型安全でありながら、printf系の利便性を両立しています。書式の仕様的にはC# (.NET) のString.Format()に近い印象です。また、Boost.Formatとは異なり、特別な演算子オーバーロードを使用せず、可変引数テンプレートで実装されているようです。
しかし、ということは実引数の型の並び(シグネチャ)ごとにテンプレートがインスタンス化される(最悪の場合、呼び出しごとにインスタンス化される)ので、組み合わせ爆発とまではいかなくても、多用すると関数実体の数が膨大になってしまいそうな気がしますが……
(従来のCの可変引数を受け取る関数の場合、実体は常に1つだけ)
インライン展開や単一定義が強制されるのであればともかく、そうでないのであればメモリには優しくないライブラリだと思います。

あるいはextern templateを使え、とでもいうのでしょうか。

ちなみにC#の可変引数は、paramsキーワードを伴う任意の型の一次元配列を受け取るメソッドにより実現されます。String.Format()の場合、object型の配列を受け取るため、値型はボクシングされます。実行時のオーバーヘッドは多少犠牲になりますが、型安全でありながら、実引数の型の並びに応じてメソッド実体の数が爆発するようなことはありません。参照型だけでなく値型を含むすべての型がオブジェクトであり、Object.ToString()メソッドを実装しているというC#の設計が、登場当初から優れていたことが証明されました。

あと、C++20のstd::format()char16_tstd::u16stringに対応していないので、実践的な(Win/Mac/iOS/Androidの)アプリケーション開発で使える場面があるかと言われると微妙です。我々が一番必要としているのは、オンメモリの内部表現として一番よく使われているUTF-16への対応なのに、C/C++標準化委員会の連中はいまだ何も分かっていません。彼らは実践的なアプリケーションソフトウェアをひとつも開発したことがないのでしょう。

*1:Boost.Format を使えばかなり改善されるんですが、C++11 においても標準ライブラリには取り込まれていないマイナーライブラリなのが欠点です。また、std::string/std::wstring の違いはやはりプログラマーが意識する必要があります。

*2:ただし、char16_tもchar32_tも最小のサイズが保証されるだけで、型のサイズがそれぞれ2バイトと4バイトにキッチリ規定されるわけではありません。相変わらずいい加減で、極めて残念な規格です。

*3:ちなみにsizeof(char)は常に1であることが規格で保証されますが、1byteが8bitであることは保証されません。これは新しい規格においても改善されておらず、C/C++はいつまで経っても曖昧で雑な言語仕様を引きずったままです。

コンテナと値型・ポインタ型

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

STL コンテナ クラス テンプレート(std::vector, std::list など)や MFC/ATL コレクション クラス テンプレート(CArray / CAtlArray, CList / CAtlList)のテンプレート引数に、値型をつかうかポインタ型を使うか迷ったことはありませんか?

もちろんポリモーフィズムを使うときは当然ポインタ型を使うしかないわけですが、例えば、int や double などのプリミティブ型のほか、以下のような比較的小さい構造体 Vector3F を引数にするときは、迷わずテンプレート引数に値型を選ぶと思います。

struct Vector3F
{
  float x, y, z;
};

class Polyline
{
public:
  std::wstring Name;
public:
  std::vector<Vector3F> PositionsArray;
  //std::vector<Vector3F*> PositionPtrsArray; // こちらは要素の寿命管理が面倒なので普通やらない。
};

std::vector は連続性が保証されているので、テンプレート引数を値型にしておけば、コンテナを通常の Vector3F の生配列として扱うこともできるし、何より値型を使うことで煩わしい new/delete を自前で記述する必要が無くなります。値型をうまく使うとデータ参照の局所性が向上することで効率も良くなります。

ですが、上記 Polyline のようなディープコピーのコストが比較的高くつくクラスをコンテナにぶち込む場合(たとえば std::vector<Polyline> など)はどうでしょうか?

Name フィールド、PositionsArray フィールドはともに可変長なので、もし長大なデータを格納していた場合、代入演算子 std::vector<Polyline>::operator=() やコピーコンストラクタによるコピーは完全なディープコピーとなって、かなり高コストになります。
これはいわゆる配列の配列=ジャグ配列(たとえば std::vector<std::vector<double>> など)にも言えることです。

特に配列コンテナをソートする場合は、要素のスワップ(交換、移動)の際に代入演算子による要素のコピーが行なわれるはずなので、ジャグ配列の要素が頻繁にコピーされる場合はすさまじいパフォーマンス低下が見込まれます(ちなみにコンテナに入れる型は、代入演算子とコピーコンストラクタが呼び出せる=public になっている必要がある)。

こういうときはポインタ型の配列 std::vector<std::shard_ptr<Polyline>> などとして、std::vector::operator=() によるコピーがハンドルのみのコピーとなるようにする手もあります。別に生ポインタの配列 std::vector<Polyline*> でも構わないんですが、寿命管理が煩雑になる(要素を削除するときは明示的にその要素を delete しないといけない)ので、パフォーマンス上どうしてもスマートポインタが使えない場合以外ではお勧めしません。

なお、C++11 (C++0x) のムーブコンストラクタが使える状況では、おそらくソート中のスワップ時にコピーコンストラクタではなくムーブコンストラクタが起動するので、テンプレート引数にスマートポインタでなく値型を使ったとしても、ソートのパフォーマンス面でのデメリットは低減できると思われます(むしろ値型を使うべき?)。

ちなみに、std::vector<T>::clear() を実行しても、予約された領域を解放するわけではない(配列の各要素に対してデストラク~T() を呼び出すが、配列自体の領域を解放するわけではない)ので、上手く使えばメモリ断片化防止になりますが、下手をすれば無駄にメモリを食ったままの状況が続きかねません。なお、std::vector では内部で placement new が使用されていて、clear() 呼び出しによって各要素に対して明示的なデストラクタ呼び出しが実行されます。このため、std::vector<std::shard_ptr<T>>::clear() を実行すると、shared_ptr のデストラクタが実行され、参照カウントがきちんと減少するようになっている模様。なかなか良くできています(MSVC の実装で確認)。

ヒープの生成と破棄を下手に繰り返すと速度低下やメモリ断片化の要因になるので使いどころが難しいんですが、単純にメモリの無駄遣いを避けたければ、swap 技法などで不要な領域を解放するか、C++11 の std::vector::shrink_to_fit() を適宜使いましょう。

なお、コンテナ末尾への要素追加だけでなく、先頭への要素追加も頻繁に行なう場合は、std::vector でなく std::deque を使うと良い(領域の拡張が行なわれない限り、どちらの操作も定数時間で完了する)と言われているのですが、VC++ における実装の場合、std::vector に比べて std::deque の末尾要素追加は2倍以上の時間がかかるので、やはりよほどの理由がない限り std::vector の採用が一番無難なようです。

余談

ついでに言っておくと、MFC/ATL の CArray / CAtlArray, CList / CAtlList は STL と比べて機能性や操作性がかなり貧弱なので、これまたよほどの理由がない限り std::vector, std::list を採用したほうが良いです。ただ、VC++ の std::vector, std::list のデバッグ バージョンでは、オーバーラン チェックが入るために、速度性能が CAtlArray, CAtlList などのそれに比べて著しく劣る(ただし std::vector の実装を見る限り、_ITERATOR_DEBUG_LEVEL で制御できる模様)ので、STL への移行を検討している人達は注意しておいたほうが良いかもしれません。リリースビルドでは当然冗長なオーバーラン チェックは入りません。

なお、std::vector, std::list, std::map などの STL コンテナは、MFC/ATL のコレクションと違って、VC++ ビジュアル デバッガーでのコンテナ内部階層の要素表示が既定で可能になっているのも重要です。MFC/ATL コレクションで同じことをやろうとすると結構設定が面倒なんですが、これもまたいつか解説しようかな……

そういえば CRT の qsort() よりも std::sort() を使うほうがよい、とよく言われていますが(関数オブジェクトがインライン化される)、int×20,000要素の乱数をぶちこんだ動的配列に対して性能比較したら、確かにリリースビルドの場合で実際2倍くらいの差が出ました。これはかなり大きい。

あと STL は配列だろうがリストだろうが同じアルゴリズム関数テンプレートが適用できるのが強みです。後からコンテナの種類を変えたくなったときも、最初に互換性のある書き方をしておけば簡単にコンテナを入れ替えることができます(テンプレートによる静的ダックタイピング)。