在升级到gcc 7.2之后,我最近遇到了一个奇怪的C ++崩溃,可以使用以下简单的完整c ++ 11程序来演示:
#include <cassert>
#include <cstdio>
struct MyObject
{
static MyObject & null_obj()
{ return *static_cast<MyObject*> (nullptr); }
operator bool()
{
return value != 0;
}
int value = 0;
};
int foo(MyObject & obj = MyObject::null_obj())
{
if (&obj != &MyObject::null_obj() && obj)
return 1;
return 0;
}
int main(int argc, char * argv[])
{
int result;
if (argc == 1)
{
result = foo();
}
else
{
MyObject obj;
obj.value = 1;
result = foo(obj);
}
printf("%d", result);
}
可以使用以下命令构建程序:g ++ -std = c ++ 11 -O2
当没有参数执行时 - 程序采用顶部分支并在使用gcc6.1或更高版本构建时崩溃。使用gcc 5.5构建时,它不会崩溃。
添加虚拟参数时 - 程序采用第二个分支,不会按预期崩溃。在没有编译器优化的情况下构建时,程序也不会崩溃。
当评估foo()中的条件时,崩溃似乎发生了。 根据短路评估规则,当表达式的第一部分为假时,不应执行此代码路径中的第二个条件。
当使用更新的编译器(例如gcc 6.1或更高版本)时,为foo生成的汇编程序如下所示:
foo(MyObject&):
mov edx, DWORD PTR [rdi]
xor eax, eax
test edx, edx
setne al
ret
崩溃的罪魁祸首是函数顶部的mov指令。 gcc 5.5的汇编程序看起来有点不同:
foo(MyObject&):
test rdi, rdi
je .L3
mov edx, DWORD PTR [rdi]
xor eax, eax
test edx, edx
setne al
ret
.L3:
xor eax, eax
ret
函数顶部的检查会跳过无效读取(如预期的那样)
有人可能会争辩说,以这种方式使用空引用是一种相当狡猾的做法,我很想同意,即使我不确切知道为什么。但是我在boost :: error_code类中遇到了同样的习惯用法,它使用了boost :: throws():docs,source。
我知道短路eval不适用于超载||的类型和&amp;&amp;运营商,但显然不是这种情况。
这是UB吗? (用户错误),编译器错误或其他什么?
更新:
我对boost :: error_code的原始引用是针对版本1.65.1的。此实现首次引入boost版本1.40。我已经发现在最新版本的增强版中,功能被修改可能是为了避免UB,但很奇怪它被允许长期不受挑战。新函数使用非零整数常量:
namespace detail
{
// Misuse of the error_code object is turned into a noisy failure by
// poisoning the reference. This particular implementation doesn't
// produce warnings or errors from popular compilers, is very efficient
// (as determined by inspecting generated code), and does not suffer
// from order of initialization problems. In practice, it also seems
// cause user function error handling implementation errors to be detected
// very early in the development cycle.
inline system::error_code* throws()
{
// See github.com/boostorg/system/pull/12 by visigoth for why the return
// is poisoned with (1) rather than (0). A test, test_throws_usage(), has
// been added to error_code_test.cpp, and as visigoth mentioned it fails
// on clang for release builds with a return of 0 but works fine with (1).
return reinterpret_cast<system::error_code*>(1);
}
}
inline system::error_code& throws() { return *detail::throws(); }
答案 0 :(得分:3)
这是未定义的行为:
{ return *static_cast<MyObject*> (nullptr); }
您不能将nullptr
转换为引用。它也打破了任何有参考资格的假设。
注意:未定义的行为意味着任何事情都可能发生(包括崩溃或不崩溃)。
答案 1 :(得分:1)
短路不会影响它。您调用了null_obj()
,这是一个取消引用空指针的函数。因此,您的程序具有未定义的行为,句点。
此规则不取决于您是否尝试稍后从所述损坏的值中读取某些数据。
如果Boost这样做,那么Boost有一个bug。虽然Boost不太可能这样做。