我知道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
中的任何垃圾值都无关紧要,因为无论如何它都会评估为真或假。”
我已经设置了Compiler Explorer example来显示反汇编中的问题,这里是完整的示例。 注意:为了解决这个问题,我发现有效的组合是通过将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 ++标准是否允许编译器假设布尔值只能使用内部数值表示“ 0”或“ 1”并使用它这样吗?
这是否是实现定义的情况,在这种情况下,实现假设所有的布尔值都只会包含0或1,而其他任何值都是未定义的行为范围?
答案 0 :(得分:55)
允许编译器假定作为参数传递的布尔值是有效的布尔值(即已初始化或转换为true
或false
的布尔值)。 true
的值不必与整数1相同-实际上,true
和false
可以有各种表示形式,但是参数必须是一些有效的表示形式这两个值之一,其中“有效表示”是实现定义的。
因此,如果您未能初始化bool
,或者通过其他类型的指针成功覆盖了content-type
,则编译器的假设将是错误的,并且将导致未定义的行为。您已被警告:
50)以本国际标准描述为“未定义”的方式使用bool值,例如通过检查未初始化的自动对象的值,可能导致其行为既不正确也不错误。 (第6.9.1节“基本类型”第6段的脚注)
答案 1 :(得分:50)
该函数本身是正确的,但是在您的测试程序中,调用该函数的语句通过使用未初始化的变量的值导致未定义的行为。
该错误位于调用函数中,可以通过对调用函数进行代码审查或静态分析来检测到。使用您的编译器浏览器链接,gcc 8.2编译器确实可以检测到该错误。 (也许您可以针对clang提交一个错误报告,指出它没有发现问题)。
未定义的行为意味着任何事情都可能发生,其中包括程序在触发未定义行为的事件后崩溃了几行。
NB。答案为“未定义的行为会导致_____吗?”始终为“是”。从字面上看,这就是未定义行为的定义。
答案 2 :(得分:22)
布尔仅允许保留值0
或1
,并且生成的代码可以假定其仅保留这两个值之一。为赋值中的三进制生成的代码可以使用该值作为指向两个字符串的指针数组的索引,即,它可以转换为类似以下内容的
// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];
如果boolValue
未初始化,则它实际上可以保存任何整数值,这将导致在strings
数组的边界之外进行访问。
答案 3 :(得分:15)
很多问题在总结,您在问C ++标准是否允许编译器假设bool
只能使用内部数字表示形式'0'或'1'并以此方式使用它?
该标准没有说明bool
的内部表示。它仅定义将bool
转换为int
时发生的情况(反之亦然)。通常,由于这些整数转换(并且人们非常依赖它们),编译器将使用0和1,但不必(尽管它必须遵守使用的任何较低级别ABI的约束) )。
因此,编译器在看到bool
时有权考虑所说的bool
包含'true
'或'false
'位模式之一,以及做任何感觉的事情。因此,如果true
和false
的值分别为1和0,则确实允许编译器将strlen
优化为5 - <boolean value>
。其他有趣的行为也是可能的!
正如这里反复提到的那样,未定义的行为会产生未定义的结果。包括但不限于
请参见What every programmer should know about undefined behavior