是否允许 C++ 编译器在评估其参数之前将函数 ptr 存储在寄存器中?
Is C++ compiler allowed to store function ptr in register before its arguments are evaluated?
我和我的同事正在与我们正在开发的应用程序中的一个相当奇怪的错误作斗争。最终我们修复了它,但我们仍然不确定编译器所做的是否合法。
假设我们有这样的代码:
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;
}
Visual Studio x86 编译器,对于函数 A::bar
,在使用 /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 获得类似的程序集。
虽然求值顺序在 C++17 之前是特定于实现的,但 C++17 强加了一些顺序,请参阅 evaluation order。
所以在
this->member->foo(renew());
renew()
可能会在评估 this->member
(C++17 之前)之前被调用。
为了保证之前的顺序,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);
为了清楚起见,这里是已经链接的 Order of evaluation 文档 Jarod42,以及相关引用:
14) In a function-call expression, the expression that names the function is sequenced before every argument expression and every default argument.
所以我们应该阅读声明
return member->foo(renew());
作为
return function-call-expression;
其中 function-call-expression 是
{function-naming-expression member->foo} ( {argument-expression renew()} )
所以,function-naming-expressionmember->foo
就是sequenced-before的参数表达式。已链接的文档说
If A is sequenced before B, then evaluation of A will be complete before evaluation of B begins.
所以我们必须先完全评估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 编译器是正确的,即使它有效,该代码也会令人讨厌。
我和我的同事正在与我们正在开发的应用程序中的一个相当奇怪的错误作斗争。最终我们修复了它,但我们仍然不确定编译器所做的是否合法。
假设我们有这样的代码:
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;
}
Visual Studio x86 编译器,对于函数 A::bar
,在使用 /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 获得类似的程序集。
虽然求值顺序在 C++17 之前是特定于实现的,但 C++17 强加了一些顺序,请参阅 evaluation order。
所以在
this->member->foo(renew());
renew()
可能会在评估 this->member
(C++17 之前)之前被调用。
为了保证之前的顺序,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);
为了清楚起见,这里是已经链接的 Order of evaluation 文档 Jarod42,以及相关引用:
14) In a function-call expression, the expression that names the function is sequenced before every argument expression and every default argument.
所以我们应该阅读声明
return member->foo(renew());
作为
return function-call-expression;
其中 function-call-expression 是
{function-naming-expression member->foo} ( {argument-expression renew()} )
所以,function-naming-expressionmember->foo
就是sequenced-before的参数表达式。已链接的文档说
If A is sequenced before B, then evaluation of A will be complete before evaluation of B begins.
所以我们必须先完全评估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 编译器是正确的,即使它有效,该代码也会令人讨厌。