如何正确重写ASSERT代码以在msvc中传递/分析?

sor*_*rin 10 c++ assert code-analysis visual-c++

Visual Studio /analyze为C/C++ 添加了代码分析(),以帮助识别错误的代码.这是一个非常好的功能,但是当您处理旧项目时,您可能会被警告的数量所淹没.

大多数问题都在产生,因为旧代码在方法或函数的开头做了一些ASSERT.

我认为这是代码中使用的ASSERT定义(来自afx.h)

#define ASSERT(f)          DEBUG_ONLY((void) ((f) || !::AfxAssertFailedLine(THIS_FILE, __LINE__) || (AfxDebugBreak(), 0)))
Run Code Online (Sandbox Code Playgroud)

示例代码:

ASSERT(pBytes != NULL);
*pBytes = 0; // <- warning C6011: Dereferencing NULL pointer 'pBytes'
Run Code Online (Sandbox Code Playgroud)

我正在寻找一种简单,清洁和安全的解决方案来解决这些警告,这并不意味着禁用这些警告.我是否提到当前代码库中出现了很多事件?

jal*_*alf 5

/analyze不保证会产生相关和正确的警告.它可以并且将会遗漏很多问题,并且它也会产生许多误报(它被识别为警告,但它们非常安全且永远不会发生)

期望使用/ analyze进行零警告是不现实的.

它指出了一种情况,你取消引用无法验证的指针始终有效.就PREfast而言,不能保证永远不会为NULL.

但这并不意味着它可以是NULL.只是证明它安全的分析对于PREfast来说过于复杂.

您可以使用特定于Microsoft的扩展__assume来告诉编译器它不应该产生此警告,但更好的解决方案是保留警告.每次使用/ analyze进行编译时(每次编译时都不需要),您应该验证它确实出现的警告仍然是误报.

如果您使用正确的断言(在编程时赶上逻辑错误,防范这情况不能发生,那么我看不出有什么问题,你的代码,或者离开的警告.添加代码来处理的问题可能永远不会发生仅仅是毫无意义的.你无缘无故地添加更多的代码和更多的复杂性(如果它永远不会发生,那么你无法从中恢复,因为你完全不知道程序将处于什么状态.你所知道的是它有它进入你认为不可能的代码路径.

但是,如果您将断言用作实际错误处理(在特殊情况下该值可能为NULL,您只是希望它不会发生),那么它就是代码中的缺陷.然后需要适当的错误处理(通常是例外).

永远不要使用断言来解决可能出现的问题.使用它们来验证不可能发生的事情.当/ analyze为您提供警告时,请查看它们.如果它是误报,请忽略它(不要压制它,因为虽然它今天是误报,但明天检查的代码可能会把它变成一个真正的问题).

  • 我认为,在允许签入之前,应该要求编译在最高警告级别(包括/分析)上进行零错误和零警告编译.很多原因,但一个非常简单的原因是,如果允许保留警告,那么最终可能会有数百或数千个警告.期望每次进行生产构建时都要检查每个人的有效性是不合理的.它根本不会发生,首先就会破坏警告的目的. (2认同)
  • @John:这就是你通常不用/ analyze编译的原因.但是,当然,如果你喜欢零警告,即使使用/ analyze,也可以在确认为误报后立即停止警告.我的主要观点是,您不需要处理永远不会发生的问题.你是否对它的警告保持沉默取决于你.我宁愿保留它,只是偶尔运行/分析,因为PREfast不是*假设*是准确的或产生零误报.它应该是偏执的,所以我想*偶尔*验证它的输出. (2认同)

Joh*_*ing 3

PREFast 告诉您代码中存在缺陷;不要忽视它。事实上你确实有一个,但你只是匆匆地承认了这一点。问题是这样的:仅仅因为pBytes在开发和测试中从未为 NULL 并不意味着它不会投入生产。你不会处理这种可能性。PREfast 知道这一点,并试图警告您,生产环境是充满敌意的,并且会让您的代码变成一堆冒烟、残缺不全、毫无价值的字节。

/咆哮

有两种方法可以解决这个问题:正确的方法和黑客。

正确的方法是在运行时处理 NULL 指针:

void DoIt(char* pBytes)
{
    assert(pBytes != NULL);
    if( !pBytes )
        return;
    *pBytes = 0;
}
Run Code Online (Sandbox Code Playgroud)

这将使 PREfast 安静下来。

破解方法是使用注释。例如:

void DoIt(char* pBytes)
{
    assert(pBytes != NULL);
    __analysis_assume( pBytes );
    *pBytes = 0;
}
Run Code Online (Sandbox Code Playgroud)

编辑: 这是描述 PREfast 注释的链接。无论如何,这是一个起点。

  • 断言旨在捕获逻辑错误:逻辑上永远不会发生的事情。从定义上来说,这些是不可能恢复的。您会尝试从尚未发现的问题中恢复。那么你怎么知道该怎么做才能恢复呢?假设这种情况下的断言使用得当(不是作为错误处理,而是为了防止开发过程中的逻辑错误),并且假设该值在逻辑上不能为“NULL”,那么绝对没有理由处理它是“的情况”空`。 (9认同)
  • 呃,你盲目地假设“pBytes”*可以*实际上是“NULL”。这不一定是一个有效的假设。PREfast 只是谨慎行事:它经常给出误报。这就是默认情况下不启用它的原因。它会告诉您它发现的可疑内容,然后由您来验证它是否正确,如果是,请解决问题。但是仅仅因为 PREfast 这么说就说你实际上需要处理 NULL 情况纯粹是垃圾。如果它实际上在逻辑上可以发生,就处理它。 (8认同)
  • @John Dibling - 发布版本中的私有方法不必每次都检查其每一个参数。这是对资源的浪费。首先应该正确调用该方法。每次。如果您无法通过检查来验证私有方法是否被正确使用,那么您就没有正确编码。如果你永远不能假设私有方法的参数是正确的,那么 ASSERT 到底有什么用呢?在这种情况下,我们应该使发布版本与调试版本相同,并始终一遍又一遍地检查相同的值是否为 NULL。 (6认同)
  • @约翰:是的,事实上它确实触动了神经。我认为不理解自己的代码是一种不好的编程习惯。盲目地检查每一个错误情况,即使是那些不可能发生的错误情况,也无助于您编写更健壮的软件。您可以通过处理“可能”发生的错误来实现强大的软件,而无需处理所有“不可能”发生的错误情况,从而增加巨大的复杂性。更多的复杂性并不会产生更安全的代码。 (4认同)
  • @John:确实,我发布的示例不会失败。因此,“使用指针而不检查它”并不*总是*鲁莽。如果你能证明它是安全的,那么失败的情况就不需要处理。这就是我的观点。显然,如果你不能证明它总是安全的,那么你是对的,它必须被处理。我的观点很简单,*如果*它是安全的,那么您就是在浪费时间来处理一个不存在的问题。(您不妨尝试验证 2+2=4,或者当我调用函数 `foo()` 时,不会调用 `bar()` 。) (2认同)