首页 > 解决方案 > 将 COM 接口智能指针传递给函数时出现警告 C26415 / C26418

问题描述

这是一个函数的定义:

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

参数MSAToolsLibrary::IAvailabilityPtr设计如下:

_COM_SMARTPTR_TYPEDEF(IAvailability, __uuidof(IAvailability));

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

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

使用智能指针类型将数据传递给函数表明目标函数需要管理所包含对象的生命周期。但是,如果函数只使用智能指针来访问包含的对象,并且从不实际调用任何可能导致其释放的代码(即从不影响其生命周期),则通常不需要使用智能指针使接口复杂化。首选包含对象的普通指针或引用。

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

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

另一个警告是:

我注意到它说:

如果共享指针参数通过值或引用传递给常量对象,则预期该函数将控制其目标对象的生命周期,而不会影响调用者。代码应该将共享指针参数复制或移动到另一个共享指针对象,或者通过调用接受共享指针的函数将其进一步传递给其他代码。如果不是这种情况,那么纯指针或引用可能是可行的。

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

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);

它说我有语法错误。

标签: visual-c++mfccomsmart-pointerscode-analysis

解决方案


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

COM 基础

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

更智能的 COM

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

激励例子

为了让这个答案自成一体,让我们编写一个简单的程序来输出桌面的 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):警告 C26415:智能指针参数“壁纸”仅用于访问包含的指针。改用 T* 或 T& (r.30)。
main.cpp(10):警告 C26418:共享指针参数“壁纸”未复制或移动。请改用 T* 或 T& (r.36)。

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

代码分析,呃,分析

C26418在这里更为重要(尽管C26415被宣传为优先问题)。代码分析器识别_com_ptr_t为智能指针类型,但发现没有使用任何特殊功能(复制/移动构造函数、复制/移动赋值)。因此得出结论,没有通过智能指针类型进行生命周期管理,并建议改用原始指针/引用。

观察结果是准确的,建议也基本没问题,只有一条皱纹(见下文)。

应用建议的修复

建议的解决方法是“使用T*T&替代”。通过_com_ptr_t提供隐式转换运算符,Interface&我们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&()由于 of和operator Interface*()of的语义不同,函数实现也不同_com_ptr_t。前者执行空指针检查,而后者不执行,将责任传递给函数实现。

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

胜利

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

嗯,不。并不真地。虽然代码分析器非常擅长识别智能指针类型,但它也假设所有生命周期管理都是通过智能指针的特殊成员函数处理的。_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函数)尝试清理时,它通过不再指向有效内存的指针访问内存。最好的情况是访问冲突,最终终止程序。然而,在更复杂的程序中,这是一个即时堆损坏错误。

代码分析警告,你问?没有任何。可以说,这是代码分析器中的一个缺陷,虽然它被识别_com_ptr_t为智能指针,但在调用 COM 生命周期管理语义时却扮演着老年痴呆症的角色。

解决方案?

据我所知,没有任何一个是全方位安全的。没有什么是可以静态验证的,并且在面对未来的变化时可以很好地适应代码。我个人的看法是始终按值传递包装在智能指针后面的 COM 接口指针。

这将是有争议的,因为_com_ptr_t按值传递会产生副本,这并不总是严格要求的。调用复制构造函数会导致调用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()and的所有显式调用Release()。使用智能指针时,两者都不需要(很像newdelete不是),而且任何一个都是潜在的错误。

结论

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


1 移动构造函数和移动赋值运算符的特殊之处在于它们转移了所有权。两者都没有调用,AddRef()也没有调用Release()它们各自的参数,因为引用计数没有变化。接口指针只是被移动到一个新的家。


推荐阅读