【DirectX12】ComPtrで詰まったところとか

お疲れ様です。ねににみみずです。

『DirectX12の魔導書』にておすすめされたMicrosoft::WRL::ComPtr<T>クラス。(以下ComPtr)
DirectXを使用する際に大量生成するCOMオブジェクトの管理を助けてくれる大変便利なスマートポインタなのですが、使い方を誤るとメモリの解放漏れが発生します。

この記事では、自分が引っ掛かったところを中心にComPtrの注意点を備忘録的に紹介していきます。

COMオブジェクトについて

私もこの概念についてはフワッとしか理解していないのですが、この記事ではCOMオブジェクトを、DirectXプログラミングで使用するインターフェース(ID3D12Deviceなど)のポインタが指し示す先にある実体の総称として扱います。

完全にインターフェースを介して使用されるオブジェクトなので、実体に直接触れることができません。また、実体を補足できないのでC++のスコープの概念からも切り離されています。new演算子でヒープに確保したクラスと同じようなものです。
そのため、要らなくなったら明示的に破棄してあげる必要があります。

間違っていたらすみません。
DirectXとCOMの関係については以下の記事に解説があります。激ムズです。

参照カウントについて

先ほどCOMオブジェクトは明示的に破棄してあげる必要があると書きましたが、そのための仕組みとして参照カウントという値があります。自分が他のいくつのオブジェクトから参照されているのかを表す数値で、各インターフェースのAddRefメソッドで増加しReleaseメソッドで減少します。
カウントが0になったときに、COMオブジェクトは自身を削除します。これによって、まだ使用しているのにポインタ先のオブジェクトが破棄されてしまうというすれ違いを防いでくれています。

そしてComPtrはCOMオブジェクト専用のスマートポインタクラスで、インターフェースのポインタを代入すると自動的にAddRefメソッドを呼んでくれます。もちろんResetメソッドやデストラクタでポインタ先のReleaseメソッドを呼んでくれます。COMオブジェクトの所有権をComPtrオブジェクトの寿命と紐付けているわけですね。

初期化方法に気をつけろ

さて、一見するとスマートポインタとしては全うな挙動ですが、この参照カウントをスマートポインタではなくオブジェクト自身が持っているというのがクセでありミソです。
つまり使い勝手が似ているstd::shared_ptr<T>とは異なり、生のポインタからでも参照カウントが増減可能なのです。

たとえば、生成したてのCOMオブジェクトの参照カウントは1です(0だと破棄されてしまうため)。そして前述のとおり、ComPtrはポインタを代入するとポインタ先のAddRefメソッドを呼んでしまいます。これによって参照カウントが余分に増えてしまいます。
そのままではプログラム終了時にメモリリークが発生します。

よって、COMオブジェクトの生成の際には

  1. 生成後に使わなくなったポインタ変数からReleaseメソッドを呼び、参照カウントを1つ減らす。

  2. ComPtrのAttachメソッドにポインタを渡して初期化する(AddRefが呼ばれない)。

  3. そもそも生のポインタを使用せず、ComPtrで全て解決する。

といった対処をする必要があります。

「プログラムを終了するときに参照カウントを余分に減らす」という方法もありますが、解放順を間違えると破棄済みのメモリにアクセスしてしまう可能性がありますし、また自前で参照カウントの管理をするならComPtrを使う意義がないのでおすすめしません。

個人的にはローカル変数なども全てComPtrで対応して、コード上で生のポインタを見せないようにするのが確実かなと思います。

アドレス演算子を使うな

ComPtrにはアドレス演算子もオーバーロードされています。しかし返り値はポインタアドレスではなくComPtrの参照専用のComPtrRefクラスになっています。
そしてそのComPtrRefに実装されているポインタアドレスへの変換のオーバーロードでは、参照元のComPtrのReleaseAndGetAddressOfメソッドを呼び出しています。GetAddressOfメソッドではなく、ReleaseAndGetAddressOfメソッドです。

別のクラスを介するせいでややこしいですが、つまり単にポインタアドレスを取得しようとして所有しているインターフェースを手放してしまうというミスが起こり得ます。ディスクリプタヒープを設定するときなどですね。

ComPtrへ移行した際にコンパイルエラーが出ないため見つけにくいバグですので注意しましょう。
というか、挙動が分かりづらいのでアドレス演算子は使用しないことをおすすめします。

自分でReleaseメソッドを呼ぶな

ComPtrを使用すればAddRef/Releaseの流れを自動化できるわけですが、ちょっとしたテストとしてグローバルにポインタを置いたときやデバッグなどで明示的に破棄したい場合があります。そのときにインターフェースのReleaseメソッドを呼んではいけません。

前述のとおりAddRef/Releaseはあくまで参照カウントを増減させることを目的としているメソッドなので、ComPtrで管理しているのにインターフェース側でReleaseメソッドを呼んでしまうとカウントが崩壊します
それで参照カウントが0になれば当然COMオブジェクトは破棄されてしまい、その後ComPtrのデストラクタでインターフェースのReleaseを呼ぶ際に破棄済みのメモリを参照してしまう恐れがあります。

デストラクタでの例外スローは御法度です。私はComPtrへ移行した際にうっかり書き換え忘れてやらかしました。
特別な事情がない限りComPtrのResetメソッドを使いましょう。nullptrのチェックもメソッド内でやってくれます。

おしまい

以上でComPtrの注意点もとい詰まりポイントの話はおしまいです。こうして見るとスマートポインタというものを理解していないが故のミスが多かったなという印象です。

ComPtrはテンプレートクラスでありリファレンスなしでも内部実装が大変読みやすくなっているので、是非皆さんもお手元のライブラリで読んでみてはいかがでしょうか。
長々とお付き合いいただきましてありがとうございました。

この記事が気に入ったらサポートをしてみませんか?