为什么增强的GCC 6优化器打破实用的C ++代码?
GCC 6有一个新的优化器功能:它假定this
总是不为空,并基于this
优化。
值范围传播现在假定C ++成员函数的这个指针是非空的。 这消除了常见的空指针检查, 但也打破了一些不合格的代码库(如Qt-5,Chromium,KDevelop) 。 作为临时解决方法,可以使用-fno-delete-null-pointer-checks。 错误的代码可以通过使用-fsanitize = undefined来识别。
更改文件清楚地表明这是危险的,因为它打破了令人惊讶的常用代码量。
为什么这个新的假设会破坏实际的C ++代码? 是否有特定的模式,粗心的或不知情的程序员依赖这种特定的未定义行为? 我无法想象任何人写作if (this == NULL)
因为那太不自然了。
我想这个问题需要回答,为什么善意的人会首先写支票。
最常见的情况是,如果你有一个类是自然发生的递归调用的一部分。
如果你有:
struct Node
{
Node* left;
Node* right;
};
在C中,你可能会写:
void traverse_in_order(Node* n) {
if(!n) return;
traverse_in_order(n->left);
process(n);
traverse_in_order(n->right);
}
在C ++中,使它成为一个成员函数是很好的:
void Node::traverse_in_order() {
// <--- What check should be put here?
left->traverse_in_order();
process();
right->traverse_in_order();
}
在C ++早期(标准化之前),强调成员函数是一个函数的语法糖,其中这个参数是隐式的。 代码是用C ++编写的,转换为等效的C并编译。 甚至还有一些明确的例子,将它与null进行比较是有意义的,原始的Cfront编译器也利用了这一点。 所以来自C背景,检查的明显选择是:
if(this == nullptr) return;
注:Bjarne的Stroustrup的甚至提到,对于规则this
已在这里多年的变化
这在许多编译器上工作了很多年。 标准化发生时,这发生了变化。 而最近,编译器开始服用调用一个成员函数,其中的优势this
之中nullptr
是未定义行为,这意味着该条件总是false
,编译器可以自由地忽略它。
这意味着要对此树进行任何遍历,您需要:
在调用traverse_in_order之前执行所有检查
void Node::traverse_in_order() {
if(left) left->traverse_in_order();
process();
if(right) right->traverse_in_order();
}
这意味着如果你可以有一个空的根,也可以在每个呼叫站点检查。
不要使用成员函数
这意味着你正在编写旧的C风格的代码(也许作为一个静态方法),并明确地将它作为参数调用它。 例如。 你回到编写Node::traverse_in_order(node);
而不是node->traverse_in_order();
在呼叫现场。
我相信以符合标准的方式修复这个特定示例的最简单/最新方法是实际使用一个sentinel节点而不是nullptr。
// static class, or global variable
Node sentinel;
void Node::traverse_in_order() {
if(this == &sentinel) return;
...
}
前两个选项似乎没有吸引力,而且代码可以逃脱它,他们用this == nullptr
编写了糟糕的代码,而不是使用适当的修复。
我猜这就是这些代码库中的一些进化如何使this == nullptr
检查它们。
编辑:增加了一个合适的解决方案,因为人们似乎想为这个响应提供一个,这个响应只是作为一个例子来说明人们为什么写了nullptr检查。
编辑5月9日:添加了指向了Bjarne Stroustroup的评论上的变化this
它是这样做的,因为“实际”代码被打破并且涉及未定义的行为。 没有理由使用空this
,不是作为一个微型的优化,通常是一个非常过早另一个。
这是一个危险的做法,因为由于类层次遍历指针的调整可以把空this
成为一个非空的。 所以,在最起码,他们的方法是应该的类与空工作this
必然是一个没有基类final类:它不能从任何派生,它不能从派生。 我们正在快速离开实际到丑陋的土地。
实际上,代码不一定很难看:
struct Node
{
Node* left;
Node* right;
void process();
void traverse_in_order() {
traverse_in_order_impl(this);
}
private:
static void traverse_in_order_impl(Node * n)
if (!n) return;
traverse_in_order_impl(n->left);
n->process();
traverse_in_order_impl(n->right);
}
};
如果你有一个空树(例如root是nullptr),这个解决方案仍然依靠未定义的行为,通过调用带有nullptr的traverse_in_order。
如果树是空的,也就是一个空的Node* root
,你不应该调用它的任何非静态方法。 期。 具有类C树代码通过显式参数获取实例指针是完全正确的。
这里的论点似乎归结为需要在可以从空实例指针调用的对象上编写非静态方法。 没有这种需要。 编写这种代码的C-with-objects方法在C ++世界中仍然更好,因为它至少可以是类型安全的。 基本上, this
是一个微观优化,具有如此狭窄的使用领域,不允许它是恕我直言,完全没问题。 没有公共API应该取决于一个空this
。
更改文件清楚地表明这是危险的,因为它打破了令人惊讶的常用代码量。
该文件并不称之为危险。 它也没有声称它打破了令人惊讶的数量的代码。 它只是指出了一些流行的代码库,它声称已知依赖于这种未定义的行为,并会因为更改而中断,除非使用了解决方法选项。
为什么这个新的假设会破坏实际的C ++代码?
如果实际的c ++代码依赖于未定义的行为,那么对该未定义行为的更改可能会破坏它。 这就是为什么要避免UB的原因,即使依赖它的程序似乎按预期工作。
是否有特定的模式,粗心的或不知情的程序员依赖这种特定的未定义行为?
我不知道它是否广泛传播反模式,但一个不知情的程序员可能会认为他们可以通过执行崩溃来修复他们的程序:
if (this)
member_variable = 42;
当实际的bug在其他地方解引用空指针时。
我确信,如果程序员不够了解,他们将能够提出更多依赖于此UB的高级(反)模式。
我无法想象任何人写作if (this == NULL)
因为那太不自然了。
我可以。
链接地址: http://www.djcxy.com/p/15059.html上一篇: Why does the enhanced GCC 6 optimizer break practical C++ code?