我和我的同事们在开发一个应用程序时遇到了一个很奇怪的错误。最终,我们将其修复,但我们仍然不确定编译器的操作是否合法。
假设我们有这样的代码:
class B {
public:
virtual int foo(int d) { return d - 10; }
};
class C : public B {
public:
virtual int foo(int d) { return d - 11; }
};
class A {
public:
A() : count(0) { member = new B;}
int bar() {
return member->foo(renew());
}
int renew() {
count++;
delete member;
member = new C;
return count;
}
private:
B *member;
int count;
};
int square() {
A a;
cout << a.bar() << endl;
return 0;
}
对于功能A::bar
,Visual Studio x86编译器在使用/O1
进行编译时会生成类似的内容(您可以在godbolt上查看完整的代码):
push esi
push edi
mov edi, ecx
mov eax, DWORD PTR [edi] ; eax = member
mov esi, DWORD PTR [eax] ; esi = B::vtbl
call int A::renew(void) ; Changes the member, vtable and esi are no longer valid
mov ecx, DWORD PTR [edi]
push eax
call DWORD PTR [esi] ; Calls wrong stuff (B::vtbl[0])
pop edi
pop esi
ret 0
此优化是标准允许的还是不确定的行为? 我无法通过GCC或clang获得类似的程序集。
答案 0 :(得分:2)
为清晰起见,下面是Order of evaluation文档Jarod42的链接,以及相关的引用:
14)在函数调用表达式中,命名函数的表达式在每个参数表达式和每个默认参数之前排序。
所以我们应该阅读声明
return member->foo(renew());
为
return function-call-expression;
其中 function-call-expression 是
{function-naming-expression member->foo} ( {argument-expression renew()} )
因此, function-naming-expression member->foo
是先于自变量表达式。已经链接的文档说
如果A在B之前排序,那么对A的评估将在对B的评估开始之前完成。
所以我们必须首先完全评估member->foo
。我认为它应该像
// 1. evaluate function-naming-expression
auto tmp_this_member = this->member;
int (B::*tmp_foo)(int) = tmp_this_member->foo;
// 2. evaluate argument expression
int tmp_argument = this->renew();
// 3. make the function call
(tmp_this_member->*tmp_foo) ( tmp_argument );
...这就是您所看到的。这是C ++ 17所要求的排序,在此之前,排序和行为均未定义。
tl; dr 编译器是正确的,即使有效,该代码也会令人讨厌。
答案 1 :(得分:1)
尽管评估的顺序是C ++ 17之前特定于实现的,但是C ++ 17施加了一些顺序,请参见evaluation order。
如此
this->member->foo(renew());
在评估renew()
(C ++ 17之前的版本)之前,可能会先调用 this->member
。
要保证C ++ 17优先订购,您必须拆分成几个不同的语句:
auto m = this->member;
auto param = renew(); // m is now pointing on deleted memory
m->foo(param); // UB.
或者,对于其他顺序:
auto param = renew();
this->member->foo(param);