2014年2月15日追記:Visual C++ 2012でこの問題は修正されました。参考:CComPtrのバグが直っていた


ATL::CComPtrをCOMインタフェース以外のクラスで使うのは要注意です。具体的には、1つ目のVTableがIUnknownで始まっていない場合、代入演算子が正しく動作しません。

#define _ATL_NO_AUTOMATIC_NAMESPACE
#include <windows.h>
#include <atlbase.h>
 
struct Foo
{
  virtual void f() = 0;
};
 
struct Bar : public Foo, public IUnknown
// struct Bar : public IUnknown, public Foo
// これだと問題ない
 
{
  virtual void f() override {};
 
  // 以下の実装は本題ではないので中身は適当に省略。
 
  virtual HRESULT STDMETHODCALLTYPE
  QueryInterface(REFIID, void**) throw() override
  {
    return E_NOTIMPL;
  };
 
  virtual ULONG STDMETHODCALLTYPE
  AddRef() throw() override
  {
    return 1;
  }
 
  virtual ULONG STDMETHODCALLTYPE
  Release() throw() override
  {
    return 1;
  }
};
 
int main()
{
  ATL::CComPtr<Bar> bar1(new Bar); // 動く
  ATL::CComPtr<Bar> bar2 = bar1; // 動く
 
  ATL::CComPtr<Bar> bar3;
  bar3 = bar1; // アウト
}

この理由は、CComPtrの代入演算子の実装を見ると分かります。Visual C++ 2010ではatlcomcli.hの328行目です。

return static_cast<T*>(AtlComPtrAssign((IUnknown**)&p, lp));

pというのは、CComPtrのメンバ変数です。そして、AtlComPtrAssignはこんな関数です。

IUnknown* AtlComPtrAssign(IUnknown** pp, IUnknown* lp);

やっていることはほぼ*pp = lpです(正確には、それに加えて適切にAddRef/Releaseを呼びます)。

AtlComPtrAssignの2番目の実引数でIUnknownへのアップキャストが発生し、それをそのままpに代入することになっているのが原因です。複数の基底クラスそれぞれに対応するVTableが作られるため、Bar b;とあったとき、(intptr_t)&b != (intptr_t)(IUnknown*)&bとなるからです。この辺りのからくりはC++: 水面下の仕組み(Visual C++ 6のドキュメント)が参考になります。

「基底クラスを指定する順番に気をつけよう」では再発が怖いので、今後COMインタフェース以外はこのような心配のないboost::intrusive_ptrを使おうと思いました。

スポンサード リンク

この記事のカテゴリ

  • ⇒ CComPtrをCOMインタフェースでないクラスに対して使うのは要注意
  • ⇒ CComPtrをCOMインタフェースでないクラスに対して使うのは要注意
  • ⇒ CComPtrをCOMインタフェースでないクラスに対して使うのは要注意