CComPtr 和 std::shared_ptr 互操作性

CComPtr and std::shared_ptr interoperability

考虑以下场景。有一个 ATL 包装的 C++ class(CMyComClass 实现了 IMyComClass),这个 class 用于异步操作,因此它(应该派生)派生自 std::enable_shared_from_this提供 shared_from_this() (而不是将 this 传递给异步函数),这将确保在调用异步操作时对象仍然存在。另一方面,有一个 COM 接口,例如可以要求将上面的 class 作为 com 对象返回,例如 get(IUnknown** myobject)addObject(IUnkown* mynewobject) 来添加对象。在这种情况下,我陷入了僵局,我不能只从 COM 获取原始指针并分配给 shared_ptr,因为我没有转让所有权,并且 shared_ptr 的引用计数将是错误的,因为它不计算以前的 COM 引用,此外 shared_ptr 计数的增加不会影响 CComPtr 引用计数,这意味着指针可能随时被销毁。此外,还有 CMyComClass 的成员函数可以创建,例如 std::async 操作,将 this 传递给它,同样,包装 COM 可能会被破坏,我将留下悬空指针。有没有办法克服这个问题?在 IUnknown 上是否有等价于 shared_from_this 的东西?
是的,我知道,设计有缺陷,不,我现在不能改变它

编辑001: 我想我把问题复杂化了。 让我们从根本问题开始。 考虑关注 COM class

class ATL_NO_VTABLE CMyComObject
    : public CComObjectRootEx<CComMultiThreadModel>,
      public CComCoClass<CMyComObject, &CLSID_MyComObject>,
      public IDispatchImpl<IMyComObject, &IID_IMyComObject, &LIBID_ATLProject2Lib, /*wMajor =*/1, /*wMinor =*/0>
{
public:
    CMyComObject() { m_pUnkMarshaler = NULL; }

    DECLARE_REGISTRY_RESOURCEID(IDR_MYCOMOBJECT)

    BEGIN_COM_MAP(CMyComObject)
    COM_INTERFACE_ENTRY(IMyComObject)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY_AGGREGATE(IID_IMarshal, m_pUnkMarshaler.p)
    END_COM_MAP()

    DECLARE_PROTECT_FINAL_CONSTRUCT()
    DECLARE_GET_CONTROLLING_UNKNOWN()

    HRESULT FinalConstruct()
    {
        m_asyncTask = std::async(std::launch::async, [self{this}]() { std::cout << typeid(self).name() << std::endl; });
        return CoCreateFreeThreadedMarshaler(GetControllingUnknown(), &m_pUnkMarshaler.p);
    }

    void FinalRelease() { m_pUnkMarshaler.Release(); }

    CComPtr<IUnknown> m_pUnkMarshaler;

private:
    std::future<void> m_asyncTask;
};

注意FinalConstruct中的std::async(不是最合适的地方,但假装它是一个普通的COM方法),async被调用,任务被调度,然后是实例(COM 对象的实例)被销毁,例如因为实例的引用计数降为零。显然计划任务将失败,比方说访问冲突。如何防止它发生?

EDIT002:只为 lulz
瞧,解决方案!

class ATL_NO_VTABLE CMyComObject
    : public CComObjectRootEx<CComMultiThreadModel>,
      public CComCoClass<CMyComObject, &CLSID_MyComObject>,
      public IDispatchImpl<IMyComObject, &IID_IMyComObject, &LIBID_ATLProject2Lib, /*wMajor =*/1, /*wMinor =*/0>
{
public:
    CMyComObject()
        : m_pUnkMarshaler(nullptr), m_self(this, [](CMyComObject* p) {
              // check if still have COM references
              if(p->m_dwRef == 0)
                  delete p;
          })
    {
    }

    DECLARE_REGISTRY_RESOURCEID(IDR_MYCOMOBJECT)

    BEGIN_COM_MAP(CMyComObject)
    COM_INTERFACE_ENTRY(IMyComObject)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY_AGGREGATE(IID_IMarshal, m_pUnkMarshaler.p)
    END_COM_MAP()

    DECLARE_PROTECT_FINAL_CONSTRUCT()
    DECLARE_GET_CONTROLLING_UNKNOWN()

    HRESULT FinalConstruct()
    {
        m_asyncTask = std::async(
            std::launch::async, [self{SharedFromThis()}]() { std::cout << typeid(self).name() << std::endl; });
        return CoCreateFreeThreadedMarshaler(GetControllingUnknown(), &m_pUnkMarshaler.p);
    }

    void FinalRelease() { m_pUnkMarshaler.Release(); }

    ULONG InternalRelease()
    {
        if(m_dwRef > 0)
        {
            _ThreadModel::Decrement(&m_dwRef);
        }
        // Dont let COM delete the instance if there is shared ptrs in the wild
        return m_dwRef + m_self.use_count();
    }
    std::shared_ptr<CMyComObject> SharedFromThis() { return m_self; }

    CComPtr<IUnknown> m_pUnkMarshaler;

private:
    std::future<void> m_asyncTask;
    std::shared_ptr<CMyComObject> m_self;
};

这将是一个很好的优雅解决方案,效果很好。但是,class 本身持有对该对象的一个​​引用,因此 std::shared_ptr 的删除器永远不会启动。唉!做正确的事,从 COM class 中取出内容,然后将提取的内容作为 shared_ptr 保存在 COM class.

直接的方法是不混合 C++ 和 COM 引用计数器,而是为 COM 接口创建一个精简的 C++ 包装器对象,该对象本身可用于异步操作:

class t_MyComClassWrapper: std::enable_shared_from_this< t_MyComClassWrapper >
{
    private: CComPtr< IMyComClass > m_p_my_class;

    public: t_MyComClassWrapper(void)
    :    m_p_my_class(new CMyComClass)
    {}

    // TODO forward IMyComClass methods...
};

auto p_wrapper(::std::make_shared< t_MyComClassWrapper >());

此设计将确保对象不会被销毁,直到 C++ 和 COM 引用计数器都降为 0。

正如其他人所提到的,COM 对于跨线程使用接口有非常严格的规则,如果您不遵守这些规则,只会招来错误。这是许多 COM 开发人员将其核心逻辑创建为 C++ classes,然后将这些 C++ classes 包装在一个瘦 COM 对象中的众多原因之一。这是我的建议。你的核心对象应该没有任何 COM 感知的东西。如果它当前有其他 COM 接口指针的数据成员,做同样的事情——将该子对象提取到 C++ class 并为父对象的 COM 包装器拥有的子对象提供 COM 包装器对象。

class CMyObject : std::enable_shared_from_this
{
public:
    void Startup()
    {
        // non-COM stuff
        auto self = shared_from_this();
        m_asyncTask = std::async(std::launch::async, [self]() {
             std::cout << typeid(self).name() << std::endl;
        });
    }
private:
    std::future<void> m_asyncTask;
}

class ATL_NO_VTABLE CMyComObject
    : public CComObjectRootEx<CComMultiThreadModel>,
      public CComCoClass<CMyComObject, &CLSID_MyComObject>,
      public IDispatchImpl<IMyComObject, &IID_IMyComObject, &LIBID_ATLProject2Lib, /*wMajor =*/1, /*wMinor =*/0>
{
    ...
    HRESULT FinalConstruct()
    {
        m_internal = make_shared<CMyObject>();
        m_internal->Startup();
        // COM stuff
        return CoCreateFreeThreadedMarshaler(GetControllingUnknown(), &m_pUnkMarshaler.p);
    }
    ...
private:
    shared_ptr<CMyObject> m_internal;
    ...
}