在评估其参数之前,是否允许C ++编译器在寄存器中存储函数ptr?

时间:2019-06-05 08:38:27

标签: c++ visual-c++ undefined-behavior

我和我的同事们在开发一个应用程序时遇到了一个很奇怪的错误。最终,我们将其修复,但我们仍然不确定编译器的操作是否合法。

假设我们有这样的代码:

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获得类似的程序集。

2 个答案:

答案 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);