我知道c++中的“未定义行为”几乎可以让编译器做任何它想做的事情。然而,当我以为代码足够安全时,我却遇到了意外的崩溃。

在这种情况下,真正的问题只发生在使用特定编译器的特定平台上,而且只有启用了优化。

为了重现这个问题并最大限度地简化它,我尝试了几种方法。下面是一个名为Serialize的函数的摘录,它将接受bool形参,并将字符串true或false复制到现有的目标缓冲区。

如果bool形参是一个未初始化的值,这个函数是否会在代码复查中,实际上没有办法判断它是否会崩溃?

// Zero-filled global buffer of 16 characters
char destBuffer[16];

void Serialize(bool boolValue) {
    // Determine which string to print based on boolValue
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    const size_t len = strlen(whichString);

    // Copy string into destination buffer, which is zero-filled (thus already null-terminated)
    memcpy(destBuffer, whichString, len);
}

如果这段代码使用clang 5.0.0 +优化执行,它将/可能崩溃。

期望的三元运算符boolValue ?"true": "false"对我来说看起来足够安全,我假设," boolValue中的垃圾值是什么并不重要,因为它无论如何都会计算为true或false。"

我已经设置了一个编译器资源管理器的例子,显示了在拆卸的问题,这里是完整的例子。注意:为了重现这个问题,我发现使用Clang 5.0.0与-O2优化的组合是有效的。

#include <iostream>
#include <cstring>

// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
    bool uninitializedBool;

   __attribute__ ((noinline))  // Note: the constructor must be declared noinline to trigger the problem
   FStruct() {};
};

char destBuffer[16];

// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
    // Determine which string to print depending if 'boolValue' is evaluated as true or false
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    size_t len = strlen(whichString);

    memcpy(destBuffer, whichString, len);
}

int main()
{
    // Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
    FStruct structInstance;

    // Output "true" or "false" to stdout
    Serialize(structInstance.uninitializedBool);
    return 0;
}

问题是由优化器引起的:它很聪明地推断出字符串“true”和“false”的长度只差1。因此,它不是真正计算长度,而是使用bool本身的值,从技术上讲,它应该是0或1,并如下所示:

const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue;       // clang clever optimization

虽然这很“聪明”,但可以这么说,我的问题是:c++标准是否允许编译器假设bool类型只能有“0”或“1”的内部数字表示,并以这样的方式使用它?

或者这是一种实现定义的情况,在这种情况下,实现假设它的所有bool只包含0或1,任何其他值都是未定义的行为领域?


当前回答

函数本身是正确的,但是在测试程序中,调用函数的语句使用了未初始化的变量值,从而导致未定义的行为。

错误存在于调用函数中,可以通过代码审查或调用函数的静态分析检测到。使用编译器资源管理器链接,gcc 8.2编译器可以检测到这个错误。(也许您可以针对clang提交错误报告,说明它没有发现问题)。

未定义行为意味着任何事情都可能发生,包括在触发未定义行为的事件发生几行之后程序崩溃。

NB。“未定义的行为会导致_____吗?”的答案总是“是的”。这就是未定义行为的定义。

其他回答

编译器可以假设一个作为参数传递的布尔值是一个有效的布尔值(即一个已经被初始化或转换为true或false的值)。true值不必与整数1相同——实际上,true和false可以有不同的表示形式——但参数必须是这两个值之一的有效表示形式,其中“有效表示”是由实现定义的。

因此,如果你未能初始化一个bool类型,或者如果你成功地通过不同类型的指针覆盖了它,那么编译器的假设将是错误的,并将发生未定义行为。你已经被警告过了:

50)以本标准所描述的“未定义”的方式使用bool值,例如检查一个未初始化的自动对象的值,可能会导致它表现为既非真也非假。(§6.9.1第6段的脚注,基本类型)

bool只允许保存内部用于true和false的依赖于实现的值,生成的代码可以假设它只保存这两个值中的一个。

通常,实现将使用整数0表示false, 1表示true,以简化bool和int之间的转换,并使if (boolvar)生成与if (intvar)相同的代码。在这种情况下,可以想象在赋值中为三元生成的代码将使用该值作为指向两个字符串的指针数组的索引,即它可能被转换为如下内容:

// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

如果boolValue是未初始化的,它实际上可以保存任何整数值,这将导致访问字符串数组的边界之外。

c++标准是否允许编译器假设bool类型只能有一个内部数字表示‘0’或‘1’,并以这样的方式使用它?

是的,如果它对任何人都有用,这里有另一个现实世界的例子。

我曾经花了几周时间在一个大型代码库中追踪一个模糊的bug。有几个方面使它具有挑战性,但根本原因是类变量的一个未初始化的布尔成员。

有一个包含这个成员变量的复杂表达式的测试:

if(COMPLICATED_EXPRESSION_INVOLVING(class->member)) {
    ...
}

我开始怀疑,这个测试没有在应该评估“正确”的时候评估“正确”。我不记得在调试器下运行是否不方便,或者我不相信调试器,或者其他什么,但我选择了用一些调试打印输出来增强代码的蛮力技术:

printf("%s\n", COMPLICATED_EXPRESSION_INVOLVING(class->member) ? "yes" : "no");

if(COMPLICATED_EXPRESSION_INVOLVING(class->member)) {
    printf("doing the thing\n");
    ...
}

想象一下,当代码显示“no”后面跟着“doing the thing”时,我有多惊讶。

检查程序集代码可以发现,有时编译器(gcc)通过将其与0进行比较来测试布尔成员,但其他时候,它使用测试最小有效位指令。当事情失败时,未初始化的布尔变量碰巧包含值2。所以,在机器语言中,这个测试相当于

if(class->member != 0)

成功了,但考验等同于

if(class->member % 2 != 0)

失败了。布尔变量实际上同时是真和假!如果这不是未定义的行为,我不知道什么是!

函数本身是正确的,但是在测试程序中,调用函数的语句使用了未初始化的变量值,从而导致未定义的行为。

错误存在于调用函数中,可以通过代码审查或调用函数的静态分析检测到。使用编译器资源管理器链接,gcc 8.2编译器可以检测到这个错误。(也许您可以针对clang提交错误报告,说明它没有发现问题)。

未定义行为意味着任何事情都可能发生,包括在触发未定义行为的事件发生几行之后程序崩溃。

NB。“未定义的行为会导致_____吗?”的答案总是“是的”。这就是未定义行为的定义。

总结一下你的问题,你在问c++标准是否允许编译器假设bool类型只能有一个内部数字表示“0”或“1”,并以这样的方式使用它?

标准没有说明bool类型的内部表示。它只定义将bool类型转换为int类型时发生的情况(反之亦然)。大多数情况下,由于这些积分转换(以及人们相当依赖它们的事实),编译器将使用0和1,但并非必须这样做(尽管它必须尊重所使用的任何较低级别ABI的约束)。

因此,当编译器看到一个bool类型时,它有权认为该bool类型包含'true'或'false'位模式,并做任何它想做的事情。因此,如果true和false的值分别为1和0,编译器确实被允许将strlen优化为5 - <布尔值>。其他有趣的行为也是可能的!

正如这里反复强调的,未定义的行为会产生未定义的结果。包括但不限于

您的代码按照您的预期工作 你的代码会随机失败 你的代码根本没有运行。

关于未定义行为,每个程序员都应该知道什么