在宏内的 if 语句中否定表达式会产生奇怪的结果

Negating expression in if statement inside macro gives odd results

我 运行 遇到了一个有点奇怪的问题。这让我觉得答案非常明显,我只是没有看到任何东西,因为代码太简单了。

我基本上有一个名为“ASSERT”的宏来确保值不为假。如果是,它会向控制台写入一条消息并中断调试。 我的问题是,当断言从 std::string::find_first_of(...) 返回的索引不等于 std::string::npos 时,断言似乎根本不起作用。每次,断言都会失败。

我已验证发生调试中断时值不相等,所以我看不出断言是如何失败的。

在我的原始代码中,它将数据从文件读取到字符串,但问题似乎仍然存在于下面的示例中,除了一个 const std::string.

我正在做一个更大的项目,但这里有一个重现错误的最小示例(顺便说一句,我使用的是 Visual Studio 2022 和 C++17):

#include <iostream>
#include <string>

#define ASSERT(x, msg) {if(!x) { std::cout << "Assertion Failed: " << msg << "\n"; __debugbreak(); } }

int main() {

    const std::string source = "Some string that\r\n contains the newline character: \r\n...";

    size_t eol = source.find_first_of("\r\n", 0);
    ASSERT(eol != std::string::npos, "Newline not present!");
    
    // Other code...
    
    return 0;
}

请注意,即使字符串中只有一个换行符 ("\r\n"),也会发生完全相同的事情。

有趣的是,“eol”似乎在我 运行 的每个测试用例中都有正确的值。唯一错误的是断言,所以如果我忽略它并继续,一切 运行 完全符合我的预期。

我也发现了这个似乎相关的问题,但没有得到答案或结论: std::string::find_first_of does not return the expected value

已在评论中提供答案。解决方案是简单地在宏的“!x”部分中的 x 周围添加括号。这转化为以下内容(归功于 0x5453):

#define ASSERT(x, msg) {if(!(x)) { ... } }

之所以解决这个问题,是因为如果您展开原始宏,它会看起来像这样(这是它的编译方式):

if(!eol != std::string::npos) {...}

这显然是不正确的,因为您想检查整个条件是否为假。像这样:

if(!(eol != std::string::npos)) {...}

添加括号可以解决这个问题。

简单,但仍然是一个很好的提醒,要检查像这样的小事情。

这是预处理器的宏替换引擎的设计简单愚蠢的意外结果。提供给宏的表达式不会像函数那样被求值,文本会在替换过程中直接插入。

给出

#define ASSERT(x, msg) {if(!x) { std::cout << "Assertion Failed: " << msg << "\n"; __debugbreak(); } }

ASSERT(eol != std::string::npos, "Newline not present!");

将转化为

{if(!eol != std::string::npos) { std::cout << "Assertion Failed: " << "Newline not present!" << "\n"; __debugbreak(); } }

并且 ! 仅应用于 eol,将宏的预期行为更改为无意义的东西。

添加评论中推荐的额外括号

#define ASSERT(x, msg) {if(!(x)) { std::cout << "Assertion Failed: " << msg << "\n"; __debugbreak(); } }

结果

{if(!(eol != std::string::npos)) { std::cout << "Assertion Failed: " << "Newline not present!" << "\n"; __debugbreak(); } }

现在在应用 ! 和测试之前正在评估表达式。

因为 macros are "evil",并且由于宏不使用任何特殊的、位置相关的调试宏,如 __FILE____LINE__,在这种情况下我将替换宏使用函数并依靠编译器的优化来内联它。