boo*_*ife 148 c++ gcc compiler-optimization undefined-behavior
GCC 6具有一个新的优化器功能:它假定this始终不为null并基于此进行优化.
值范围传播现在假定C++成员函数的this指针是非null.这消除了常见的空指针检查,但也打破了一些不符合规范的代码库(例如Qt-5,Chromium,KDevelop).作为临时解决方法,可以使用-fno-delete-null-pointer-checks.使用-fsanitize = undefined可以识别错误的代码.
更改文档清楚地将其称为危险,因为它打破了大量频繁使用的代码.
为什么这个新假设会破坏实用的C++代码?是否存在粗心或不知情的程序员依赖于这种特定的未定义行为的特定模式?我无法想象有人写作,if (this == NULL)因为那是不自然的.
jtl*_*lim 87
我想这个问题需要回答,为什么善意的人会先写支票.
最常见的情况可能是,如果您的类是自然发生的递归调用的一部分.
如果你有:
struct Node
{
Node* left;
Node* right;
};
Run Code Online (Sandbox Code Playgroud)
在C中,你可以写:
void traverse_in_order(Node* n) {
if(!n) return;
traverse_in_order(n->left);
process(n);
traverse_in_order(n->right);
}
Run Code Online (Sandbox Code Playgroud)
在C++中,很高兴使它成为一个成员函数:
void Node::traverse_in_order() {
// <--- What check should be put here?
left->traverse_in_order();
process();
right->traverse_in_order();
}
Run Code Online (Sandbox Code Playgroud)
在C++的早期阶段(标准化之前),强调的是成员函数是this参数隐含的函数的语法糖.代码是用C++编写的,转换为等效的C并编译.甚至有一些明确的例子,比较thisnull是有意义的,原始的Cfront编译器也利用了这一点.所以来自C背景,检查的明显选择是:
if(this == nullptr) return;
Run Code Online (Sandbox Code Playgroud)
注: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();
}
Run Code Online (Sandbox Code Playgroud)
这意味着如果您可以拥有空根,也可以在每个调用站点进行检查.
不要使用成员函数
这意味着您正在编写旧的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;
...
}
Run Code Online (Sandbox Code Playgroud)前两个选项中的任何一个看起来都不具吸引力,虽然代码可以侥幸逃脱,但是他们编写了错误的代码,this == nullptr而不是使用正确的修复程序.
我猜这些代码库中的一些是如何演变为对this == nullptr它们进行检查的.
Rei*_*ica 65
它之所以这样做是因为"实用"代码被破坏并且涉及未定义的行为.this除了作为微优化之外,没有理由使用null ,通常是非常不成熟的.
这是一种危险的做法,因为由于类层次结构遍历而调整指针可以将null this转换为非null值.所以,在最起码,他们的方法都应该有一个空的上班族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);
}
};
Run Code Online (Sandbox Code Playgroud)
如果你有一个空的树(如根nullptr),该解决方案仍是通过调用traverse_in_order与nullptr依赖未定义行为.
如果树是空的,也就是null Node* root,则不应该在其上调用任何非静态方法.期.拥有类似C的树代码可以通过显式参数获取实例指针.
这里的论点似乎归结为某种程度上需要在可以从空实例指针调用的对象上编写非静态方法.没有这样的需要.在C++世界中,编写此类代码的C-with-objects方式仍然更好,因为它至少可以是类型安全的.基本上,null this是这样的微优化,具有如此狭窄的使用领域,不允许它是恕我直言完全没错.没有公共API应该依赖于null this.
eer*_*ika 35
更改文档清楚地将其称为危险,因为它打破了大量频繁使用的代码.
该文件并未将其称为危险品.它也没有声称它打破了惊人数量的代码.它只是指出一些流行的代码库,它声称已知它依赖于这种未定义的行为,并且除非使用了变通方法选项,否则会因更改而中断.
为什么这个新假设会破坏实用的C++代码?
如果实际的 c ++代码依赖于未定义的行为,那么对该未定义行为的更改可能会破坏它.这就是为什么要避免使用UB,即使依赖它的程序似乎按预期工作.
是否存在粗心或不知情的程序员依赖于这种特定的未定义行为的特定模式?
我不知道它是否是广泛传播的反模式,但是一个不知情的程序员可能会认为他们可以通过执行以下操作来修复程序崩溃:
if (this)
member_variable = 42;
Run Code Online (Sandbox Code Playgroud)
当实际的bug在其他地方取消引用空指针时.
我敢肯定,如果程序员知之甚少,他们将能够提出依赖于这个UB的更高级(反)模式.
我无法想象有人写作,
if (this == NULL)因为那是不自然的.
我可以.
Jon*_*ely 25
一些被破坏的"实用"(有趣的拼写"buggy")代码看起来像这样:
void foo(X* p) {
p->bar()->baz();
}
Run Code Online (Sandbox Code Playgroud)
并且它忘记了p->bar()有时会返回空指针的事实,这意味着取消引用它来调用baz()是未定义的.
并非所有被破坏的代码都包含显式if (this == nullptr)或if (!p) return;检查.有些情况只是没有访问任何成员变量的函数,所以似乎工作正常.例如:
struct DummyImpl {
bool valid() const { return false; }
int m_data;
};
struct RealImpl {
bool valid() const { return m_valid; }
bool m_valid;
int m_data;
};
template<typename T>
void do_something_else(T* p) {
if (p) {
use(p->m_data);
}
}
template<typename T>
void func(T* p) {
if (p->valid())
do_something(p);
else
do_something_else(p);
}
Run Code Online (Sandbox Code Playgroud)
在func<DummyImpl*>(DummyImpl*)使用空指针调用时,在此代码中,存在对调用指针的"概念性"解除引用p->DummyImpl::valid(),但实际上成员函数只返回false而不进行访问*this.这return false可以内联,因此在实践中根本不需要访问指针.所以对于一些编译器,似乎工作正常:没有用于解除引用null p->valid()的段错误,是错误的,所以代码调用do_something_else(p),它检查空指针,所以什么都不做.没有观察到崩溃或意外行为.
使用GCC 6,你仍然可以调用p->valid(),但编译器现在推断出p必须为非null的表达式(否则p->valid()将是未定义的行为)并记下该信息.优化程序使用该推断信息,以便在调用do_something_else(p)内联时,if (p)检查现在被认为是冗余的,因为编译器会记住它不是null,因此将代码内联到:
template<typename T>
void func(T* p) {
if (p->valid())
do_something(p);
else {
// inlined body of do_something_else(p) with value propagation
// optimization performed to remove null check.
use(p->m_data);
}
}
Run Code Online (Sandbox Code Playgroud)
这现在确实取消引用空指针,因此以前似乎工作的代码停止工作.
在这个例子中,错误是在func,它应该首先检查null(或者调用者永远不应该用null调用它):
template<typename T>
void func(T* p) {
if (p && p->valid())
do_something(p);
else
do_something_else(p);
}
Run Code Online (Sandbox Code Playgroud)
需要记住的重要一点是,大多数这样的优化并不是编译器说"啊,程序员测试这个指针反对null,我会删除它只是为了讨厌"的情况.会发生的是,内联和值范围传播等各种普遍优化相结合,使这些检查变得多余,因为它们是在早期检查或取消引用之后进行的.如果编译器知道函数中A点的指针是非空的,并且指针在同一函数中的稍后的B点之前没有改变,那么它知道它在B处也是非空的.当内联发生时点A和B实际上可能是最初在单独函数中的代码片段,但现在组合成一段代码,并且编译器能够在更多位置应用其指针非空的知识.这是一个基本但非常重要的优化,如果编译器不这样做,那么日常代码会慢得多,人们会抱怨不必要的分支重复测试相同的条件.
| 归档时间: |
|
| 查看次数: |
9116 次 |
| 最近记录: |