Разрешено ли компилятору C ++ сохранять функцию ptr в регистре до оценки ее аргументов? - PullRequest
3 голосов
/ 05 июня 2019

Мои коллеги и я боролись с довольно странной ошибкой в ​​приложении, которое мы разрабатываем.В конце концов мы исправили это, но мы все еще не уверены, что то, что делал компилятор, является законным или нет.

Предполагая, что у нас есть код, подобный этому:

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.

Ответы [ 2 ]

2 голосов
/ 05 июня 2019

Просто для полной ясности, вот Порядок оценки документ Jarod42, уже связанный, и соответствующая цитата:

14) В выражении вызова функции выражение, котороеназывает функцию упорядоченной перед каждым выражением аргумента и каждым аргументом по умолчанию.

Таким образом, мы должны прочитать выражение

return member->foo(renew());

как

return function-call-expression;

где выражение-вызова функции равно

{function-naming-expression member->foo} ( {argument-expression 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 компилятор прав, и этот код будетбудь противен, даже если это сработало.

1 голос
/ 05 июня 2019

Принимая во внимание, что порядок оценки зависит от реализации до C ++ 17, C ++ 17 налагает некоторый порядок, см. порядок оценки .

, поэтому в

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);
...