将 COM 接口智能指针传递给函数时出现警告 C26415 / C26418

Warnings C26415 / C26418 when passing a COM interface smart pointer to a function

下面是一个函数的定义:

void CMSATools::SetPublisherDatesNotAvailable(MSAToolsLibrary::IAvailabilityPtr pAvailability, 
                                              std::vector<COleDateTime>& rVectorDates, 
                                              DWORD dwNumDates)
{
}

参数MSAToolsLibrary::IAvailabilityPtr是这样设计的:

_COM_SMARTPTR_TYPEDEF(IAvailability, __uuidof(IAvailability));

此代码(智能指针)全部自动构建为 COM 接口的一部分。但是我收到了两个关于将它用作我自己的函数的参数的警告:

我从链接文章中了解了很多:

Using a smart pointer type to pass data to a function indicates that the target function needs to manage the lifetime of the contained object. However, if the function only uses the smart pointer to access the contained object and never actually calls any code that may lead to its deallocation (that is, never affect its lifetime), there is usually no need to complicate the interface with smart pointers. A plain pointer or reference to the contained object is preferred.

我的函数所做的就是像这样使用指针:

throw_if_fail(pAvailability->SetDatesNotAvailable(arr.parray));

另一个警告是:

我注意到它说:

If shared pointer parameter is passed by value or reference to a constant object it is expected that function will take control of its target object’s lifetime without affecting of the caller. The code should either copy or move the shared pointer parameter to another shared pointer object or pass it further to other code by invoking functions which accept shared pointers. If this is not the case, then plain pointer or reference may be feasible.

这是我使用该函数的片段(不是真正的代码):

MSAToolsLibrary::IAvailabilityPtr pAvailability = NULL;
throw_if_fail(pPublisher->get_Availability(&pAvailability));
if (pAvailability != NULL)
{
    std::vector<COleDateTime> vectorDates;
    const DWORD dwNumDates = InitDatesNotAvailableVector(vectorDates);
    theApp.MSAToolsInterface().SetPublisherDatesNotAvailable(pAvailability, vectorDates, dwNumDates);
}

我试过了:

void SetPublisherDatesNotAvailable(T* pAvailability, std::vector<COleDateTime>& rVectorDates, DWORD dwNumDates);

它说我有一个语法错误。

这个比较复杂。虽然解释代码分析诊断很容易,但提出缓解策略归结为在几害中取其轻。

COM 基础知识

COM 模拟共享所有权的概念。每个接口都跟踪未完成引用的数量,当引用计数达到零时,实现该接口的 COM 对象将被销毁。生命周期管理通过 IUnknown 接口公开,其 AddRef()Release() 成员分别递增和递减引用计数。

更智能的 COM

虽然可以手动实现生命周期管理,显式访问 IUnknown 接口,但这既乏味又容易出错。智能指针类型(例如 _com_ptr_t)通过提供自动资源管理填补了这一空白。为此,_com_ptr_t class 模板实现了以下五个特殊成员函数:复制和移动构造函数、复制和移动赋值运算符以及析构函数。这些中的每一个都会根据需要添加对 AddRef()Release() 的调用 1。这些电话是不可见的,因此了解它们的存在很重要。

激励示例

为了使这个答案独立,让我们编写一个简单的程序来输出桌面的 RGB 颜色。它展示了相同的代码分析诊断,但足够紧凑以说明可能的解决方案:

#include <ShObjIdl_core.h>
#include <Windows.h>
#include <comdef.h>
#include <comip.h>

#include <iostream>

_COM_SMARTPTR_TYPEDEF(IDesktopWallpaper, __uuidof(IDesktopWallpaper));

COLORREF background_color(IDesktopWallpaperPtr wallpaper)  // line 10
{
    COLORREF cr {};
    _com_util::CheckError(wallpaper->GetBackgroundColor(&cr));
    return cr;
}

int main()
{
    _com_util::CheckError(::CoInitialize(nullptr));

    IDesktopWallpaperPtr spWallpaper { nullptr };
    _com_util::CheckError(spWallpaper.CreateInstance(CLSID_DesktopWallpaper));

    auto const color { background_color(spWallpaper) };
    std::wcout << L"R: " << GetRValue(color)
               << L"\tG: " << GetGValue(color)
               << L"\tB: " << GetBValue(color) << std::endl;
}

运行 这通过代码分析产生以下输出:

main.cpp(10): warning C26415: Smart pointer parameter 'wallpaper' is used only to access contained pointer. Use T* or T& instead (r.30).
main.cpp(10): warning C26418: Shared pointer parameter 'wallpaper' is not copied or moved. Use T* or T& instead (r.36).

请记住上面显示的代码是完全有效的。它本质上没有错。每个表达式都得到了很好的定义,所有资源都得到了妥善管理,所有错误情况都得到了处理。

代码分析,呃,分析

C26418 is more important here (even though C26415 被宣传为优先问题)。代码分析器将 _com_ptr_t 识别为智能指针类型,但发现使用了 none 特殊函数(copy/move 构造函数,copy/move 赋值)。因此得出结论,智能指针类型不会发生生命周期管理,并建议改用原始 pointer/reference。

观察准确,建议大部分也很好,只有一处皱纹(见下文)。

应用建议的修复

建议的解决方法是 “改为使用 T*T&_com_ptr_tInterface& and Interface* 提供隐式转换运算符,我们必须做出选择。幸运的是,这是少数几个明显获胜的选择之一。

使用T*

COLORREF background_color(IDesktopWallpaper* wallpaper)
{
    if (wallpaper)
    {
        COLORREF cr {};
        _com_util::CheckError(wallpaper->GetBackgroundColor(&cr));
        return cr;
    }
    else
    {
        _com_issue_error(E_POINTER);
    }
}

使用T&

COLORREF background_color(IDesktopWallpaper& wallpaper)
{
    COLORREF cr {};
    _com_util::CheckError(wallpaper.GetBackgroundColor(&cr));
    return cr;
}

调用站点 (background_color(spWallpaper)) 不需要更改,并且与任一函数调用都表现出相同的行为。由于 operator Interface&()_com_ptr_toperator Interface*() 语义不同,函数实现也不同。前者执行空指针检查,而后者不执行,将责任传递给函数实现。

除非你有非常允许传递空指针的特定原因(例如,如果你需要noexcept),采用接口引用的实现是优越的:它建立更强的先决条件,如果违反先决条件则快速失败。就个人而言,我特别喜欢后者。

胜利

那么,这就解决了两个代码分析警告。一切都很好,对吧?对吧!?

嗯,不。并不真地。虽然代码分析器非常擅长识别智能指针类型,但它也做出假设,即 all 生命周期管理是通过智能指针的特殊成员函数处理的。 _com_ptr_t 没有建立那个不变量,如下所示:

COLORREF background_color(IDesktopWallpaper& wallpaper)
{
    COLORREF cr {};
    _com_util::CheckError(wallpaper.GetBackgroundColor(&cr));
    wallpaper.Release();  // My name is Pointer, Dangling Pointer
    return cr;
}

拐弯抹角,这很糟糕。此函数 借用 对 COM 对象的引用,但随后继续调用 wallpaper.Release() 就好像它拥有它一样,减少引用计数(从未增加!)。在上面的示例中,这是唯一的引用,当调用者(main 函数)尝试清理时,它通过不再指向有效内存的指针访问内存。最好的情况是访问冲突,最终终止程序。然而,在更复杂的程序中,这是一个即时堆损坏错误。

代码分析警告,你问? None。可以说,这是代码分析器中的一个缺陷,它在将 _com_ptr_t 识别为智能指针时,在调用 COM 生命周期管理语义时发挥了阿尔茨海默氏症的作用。

解决方案?

据我所知,没有任何一种是全方位安全的。没有什么可以静态验证的,并且在面对未来的变化时与代码一起老化。我个人的做法是始终按值 .

传递包裹在智能指针后面的 COM 接口指针

这会引起争议,因为按值传递 _com_ptr_t 会产生一个副本,这并不总是严格要求的。调用 copy constructor 会导致在包含的接口指针上调用 AddRef()。在函数退出时,绑定到参数的副本被销毁,析构函数在包含的接口指针上调用 Release()。这可能看起来像是人为的引用计数增加,这也不是严格要求的。

仍然有充分的理由这样做:

  • 它支持本地推理:任何接受 _com_ptr_t 按值 的函数在函数调用期间拥有 共享引用。它可以自由复制或传递它,并确信函数外部的代码不会使接口指针无效(模数错误)。
  • 它允许选择退出自动资源管理。由于该函数拥有一个引用,因此它可以 Detach 接口指针并将其传递给拥有所有权的人。
  • 它继续与 C++20 协程一起工作。在 C++20 之前的时代,几乎不可能使传递给函数的引用无效。就所有意图和目的而言,对本地对象的引用在函数调用的整个持续时间内都有效,无论调用堆栈有多深。协程改变了这一切,引用可以在协程的第一个挂起点失效。按值传递智能指针不受协程中途局部对象失效的影响。

就我个人而言,我会选择在此处禁用代码分析警告,并执行以下实现:

#pragma warning(suppress : 26415 26418)
COLORREF background_color(IDesktopWallpaperPtr wallpaper)
{
    COLORREF cr {};
    _com_util::CheckError(wallpaper->GetBackgroundColor(&cr));
    return cr;
}

请注意,这本身也不是那么好。仍然存在您 可以 调用 wallpaper->Release() 的明显问题,导致与采用接口引用的版本相同的问题。并且没有代码分析规则来警告你。

为了确保安全,仍然需要一些手动工作,即查找并删除对 AddRef()Release() 的所有显式调用。使用智能指针时两者都不是必需的(很像 newdelete 不需要),任何一个都是潜在的错误。

结论

这里的情况并不好,没有单一的解决方案可以解决所有问题。有很多东西需要通读,甚至还有更多需要考虑。到一天结束时,由您决定选择哪种毒药。对不起。


1 移动构造函数和移动赋值运算符的特殊之处在于它们转移所有权。没有人在各自的参数上调用 AddRef()Release(),因为引用计数没有变化。界面指针只是搬进了新家