C++ 是C++;编译器是否允许在计算函数ptr的参数之前将其存储在寄存器中?

C++ 是C++;编译器是否允许在计算函数ptr的参数之前将其存储在寄存器中?,c++,visual-c++,undefined-behavior,C++,Visual C++,Undefined Behavior,我和我的同事们在一个我们正在开发的应用程序中与一个相当奇怪的错误作斗争。最终我们修复了它,但我们仍然不确定编译器所做的是否合法 假设我们有这样的代码: B类{ 公众: 虚拟intfoo(intd){返回d-10;} }; 丙类:公共乙类{ 公众: 虚拟intfoo(intd){返回d-11;} }; 甲级{ 公众: A():计数(0){member=new B;} 整型条(){ 返回成员->foo(续订()); } int续订(){ 计数++; 删除成员; 成员=新的C; 返回计数; } 私人:

我和我的同事们在一个我们正在开发的应用程序中与一个相当奇怪的错误作斗争。最终我们修复了它,但我们仍然不确定编译器所做的是否合法

假设我们有这样的代码:

B类{
公众:
虚拟intfoo(intd){返回d-10;}
};
丙类:公共乙类{
公众:
虚拟intfoo(intd){返回d-11;}
};
甲级{
公众:
A():计数(0){member=new B;}
整型条(){
返回成员->foo(续订());
}
int续订(){
计数++;
删除成员;
成员=新的C;
返回计数;
}
私人:
B*成员;
整数计数;
};
整数平方(){
A A;

cout在C++17之前,求值顺序是特定于实现的,而C++17施加了一些顺序,请参见

所以在

在计算
this->member
(以前的C++17)之前,可能会调用
renew()

要创建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);

为了清楚起见,这里是已经链接的Jarod42文档和相关引用:


14) 在函数调用表达式中,命名函数的表达式在每个参数表达式和每个默认参数之前排序

所以我们应该读一下声明

return member->foo(renew());
作为

其中函数调用表达式是

因此,函数命名表达式
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编译器是正确的,即使它工作了,代码也会很糟糕。

你是说在C++17中,编译器必须首先完全计算
renew()
?如果是这样,那一定意味着Visual Studio编译器有一个bug,或者根本不兼容C++17。@grimple:重新检查C++17的顺序。在函数调用表达式中,命名函数的表达式在每个参数表达式和每个默认参数之前排序。“…这是否意味着表达式
member
必须在参数被赋值之前进行求值,因此在
this->member
发生变异之前进行求值?实际上,我刚刚用GCC编译了相同的代码(没有优化),它在求值
renew()之前将
member
存储在
r12
中。”
。因此,C++17标准可能严格定义了这样做。我的意思是,在C++17中,
这个->成员
应该在
续订()之前进行评估(导致UB取消引用已删除的指针)。在所有方面都达成一致。即使顺序保证是OP想要的(从C++17开始就没有,也肯定没有)Jarod的最后一个例子是完美的。也同意每一点。不幸的是,这意味着带有-O2的GCC是完全错误的,因为它生成的代码可以工作,但不应该工作。我怀疑,由于这段代码使用C++17排序生成UB,因此不需要优化器来确保结果是正常的。也就是说,调用m删除此成员后,tpm\u上的方法是非法的,因此优化器可以重新排序负载,就好像从未发生过一样。你有一个完全合法的观点,这是非常有意义的。因此,总结所有内容,不要编写讨厌的代码。
return member->foo(renew());
return function-call-expression;
{function-naming-expression member->foo} ( {argument-expression renew()} )
// 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 );