C++ 为什么增强的GCC6优化器破坏了实用的C++;密码?
:它假定此始终不为空,并基于此进行优化C++ 为什么增强的GCC6优化器破坏了实用的C++;密码?,c++,gcc,compiler-optimization,undefined-behavior,C++,Gcc,Compiler Optimization,Undefined Behavior,:它假定此始终不为空,并基于此进行优化 值范围传播现在假设C++成员函数的这个指针是非空的。这消除了常见的空指针检查,但也破坏了一些不一致的代码库(如Qt-5、Chromium、KDevelop)。作为临时解决方法,可以使用fno delete null指针检查。使用-fsanize=undefined可以识别错误代码 变更文档明确指出这是危险的,因为它破坏了大量频繁使用的代码 强>为什么新的假设会破坏实用的C++代码?有没有特定的模式,粗心或不知情的程序员依赖这个特定的未定义行为?如果(thi
值范围传播现在假设C++成员函数的这个指针是非空的。这消除了常见的空指针检查,但也破坏了一些不一致的代码库(如Qt-5、Chromium、KDevelop)。作为临时解决方法,可以使用fno delete null指针检查。使用-fsanize=undefined可以识别错误代码
变更文档明确指出这是危险的,因为它破坏了大量频繁使用的代码 <>强>为什么新的假设会破坏实用的C++代码?<强>有没有特定的模式,粗心或不知情的程序员依赖这个特定的未定义行为?如果(this==NULL),我无法想象有人会写,因为这太不自然了。之所以这样做,是因为“实用”代码被破坏了,并且一开始就涉及到未定义的行为。没有理由使用null这个
,除了作为一个微观优化,通常是一个非常不成熟的优化
这是一种危险的做法,因为它可以将空此转换为非空。因此,至少,其方法应该与nullthis
一起工作的类必须是没有基类的最终类:它不能从任何东西派生,也不能从任何东西派生。我们正在迅速从实用转向实用
实际上,代码并不一定难看:
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_
如果树是空的,也就是nullNode*root
,则不应该对其调用任何非静态方法。时期拥有通过显式参数获取实例指针的类似C的树代码是非常好的
这里的论点似乎归结为需要在可以从空实例指针调用的对象上编写非静态方法。没有这个必要。C++对象的编写这种代码的方式在C++世界中仍然更好,因为它至少可以是类型安全的。基本上,空这个是一个非常微小的优化,使用范围非常狭窄,因此不允许它是非常好的。任何公共API都不应该依赖于nullthis
我想首先需要回答的问题是,为什么善意的人会编写检查
最常见的情况可能是您有一个属于自然发生的递归调用的类
如果你有:
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();
}
注:比亚恩·斯特劳斯特鲁普甚至提到,多年来,这项
的规则已经改变
这在许多编译器上工作了很多年。当标准化发生时,情况发生了变化。最近,编译器开始利用调用成员函数的优势,其中this
beingnullptr
是未定义的行为,这意味着该条件总是false
,编译器可以随意忽略它
这意味着要遍历此树,您需要:
- 在按顺序调用
traverse\u之前执行所有检查
void Node::traverse_in_order() {
if(left) left->traverse_in_order();
process();
if(right) right->traverse_in_order();
}
这意味着还要在每个调用站点检查是否可以有空根
- 不要使用成员函数
这意味着您正在编写旧的C风格代码(可能作为静态方法),并使用对象作为参数显式调用它。您又回到了编写
Node::按顺序遍历(Node)代码>而不是节点->按顺序遍历\u()代码>在呼叫站点
- 我认为,以符合标准的方式修复这个特定示例的最简单/最简单的方法是实际使用sentinel节点,而不是
nullptr
// static class, or global variable
Node sentinel;
void Node::traverse_in_order() {
if(this == &sentinel) return;
...
}
前两个选项似乎都没有那么吸引人,虽然代码可以侥幸逃脱,但他们使用this==nullptr
编写了糟糕的代码,而不是使用适当的修复
我猜这就是为什么这些代码库中有this==nullptr
检查的原因
变更文档明确指出这是危险的,因为它破坏了大量频繁使用的代码
这份文件并没有说这很危险。它也没有声称它破坏了惊人数量的代码。它只是简单地指出了几个流行的代码库,它声称这些代码库依赖于这种未定义的行为,除非使用变通方法选项,否则这些代码库将因更改而中断
为什么这个新假设会破坏实用的C++代码?< /P>
如果实际C++代码依赖于未定义的行为,那么对未定义行为的更改会破坏它。这就是为什么要避免UB,即使依赖UB的程序似乎按预期工作
是否存在粗心或无知的程序员依赖这种特定的未定义行为的特定模式
我不知道这是否是一种广泛的反模式,但一个不知情的程序员可能会认为他们可以通过执行以下操作来修复程序崩溃:
if (this)
member_variable = 42;
当实际的bug在其他地方取消引用空指针时
我确信,如果程序员缺乏足够的信息,他们将能够提出更高级的(反)模式,这些模式依赖于这个UB
我无法想象有人会在(this==NULL)
的情况下编写,因为这太不自然了
“C++”标准在重要的方面被打破了。不幸的是,GCC开发人员没有保护用户免受这些问题的影响,而是选择使用未定义的行为作为实施边际优化的借口,即使已经向他们清楚地解释了边际优化的危害性
这里有很多
if (this)
member_variable = 42;
OPAQUEHANDLE ObjectType::GetHandle(){
if(this==NULL)return DEFAULTHANDLE;
return mHandle;
}
void DoThing(ObjectType* pObj){
osfunction(pObj->GetHandle(), "BLAH");
}
void foo(X* p) {
p->bar()->baz();
}
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);
}
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);
}
}
template<typename T>
void func(T* p) {
if (p && p->valid())
do_something(p);
else
do_something_else(p);
}