Сбой вызова функции-члена в VS2017 - PullRequest
0 голосов
/ 26 февраля 2019

Я исследовал странный сбой после перемещения определения класса в другой модуль и пришел к выводу, что компилятор запутывается в том, как определяются указатели на функции-члены.

Я не могувключите весь код, потому что это огромная программа, и я не смог воспроизвести ее на меньшем примере.

Edit: Мне удалось воспроизвести сбой на небольшом примере, поэтому яЯ редактирую весь вопрос, чтобы включить новый код и сборку.

StatesManager.h:

#pragma once

class StatesManager
{
public:
    bool action();
};

Toolbar.h:

#pragma once

class StatesManager;

class Toolbar
{
public:
    Toolbar( StatesManager* statesManager );

    void action( bool ( StatesManager::*action )( ), bool ( StatesManager::*enabled )( ) const = nullptr );

private:
    StatesManager* statesManager_;
};

Toolbar.cpp:

#include "Toolbar.h"
#include "StatesManager.h"

Toolbar::Toolbar( StatesManager* statesManager ) :
    statesManager_( statesManager )
{
}

void Toolbar::action( bool ( StatesManager::*action )( ), bool ( StatesManager::*enabled )( ) const )
{
    ( statesManager_->*action )( );
}

main.cpp:

#include "StatesManager.h"
#include "toolbar.h"

bool StatesManager::action()
{
    return true;
}

int main()
{
    StatesManager manager;
    Toolbar toolbar( &manager );
    toolbar.action( &StatesManager::action );
    return 0;
}

Когда этот код вызывается (из другого модуля), я получаю эту сборку:

    ( statesManager_->*action )( );
00007FF743771860  mov         rax,qword ptr [&action]  
00007FF743771867  movsxd      rax,dword ptr [rax+8]  
00007FF74377186B  mov         rcx,qword ptr [this]  
00007FF743771872  add         rax,qword ptr [rcx]  
00007FF743771875  mov         rcx,rax  
00007FF743771878  mov         rax,qword ptr [&action]  
00007FF74377187F  call        qword ptr [rax]  

Но если япоменяйте местами два включения вокруг или уберите второй аргумент из функции, я получу совершенно другую разборку:

    ( statesManager_->*action )( );
00007FF68CB01860  mov         rax,qword ptr [this]  
00007FF68CB01867  mov         rcx,qword ptr [rax]  
00007FF68CB0186A  call        qword ptr [action]  

Первый код вылетает в инструкции вызова.Он пытается прочитать значение dword в &action+8, которое никогда не было инициализировано, и приводит к сбою в инструкции call.

Я обнаружил ошибку, связанную с , полгода назад , но это должно быть исправлено в 15.9, когда я в данный момент на 15.9.7.

Это еще одна ошибка в VS2017 или я делаю что-то непреднамеренное с указателями на функции-члены и объявлениями forward?

1 Ответ

0 голосов
/ 26 февраля 2019

Я почти уверен, что проблему можно решить с помощью опции /vmg.

В противном случае компилятор оптимизирует представление указателей на элементы в зависимости от определения класса.Классу с несколькими базовыми классами нужны разные указатели-члены, чем классу без них, а классу с виртуальными базовыми классами могут потребоваться даже более сложные.

Без /vmg компилятор будет генерировать другой код в зависимости от того, имеет ли онвидел полное определение IStatesManager, которое по имени я предполагаю как интерфейс с виртуальными методами.

Все модули, использующие этот класс, должны быть также скомпилированы с параметром / vmg, поэтому правильноеуказатель типа члена передается внутрь.

В качестве альтернативы, вы, возможно, могли бы включить заголовок для IStatemanager в заголовок ControlNode, но я предполагаю, что предварительное объявление было использовано намеренно для уменьшения зависимостей.

Редактировать: компилятор по-прежнему оптимизирует указатель метода, вызывающий код, когда он знает определение класса, и, таким образом, может исключить сложный случай виртуального вывода, как указано в комментариях, важное отличие заключается в инициализацииуказатели метода, который гарантировандля соответствия /vmg.

Код, сгенерированный для этих функций, показывает разницу:

struct VirtMethods
{
  virtual int m();
};

struct VDerived : public virtual VirtMethods
{
  virtual int m() override;
};

int invokeit2(VirtMethods &o, int (VirtMethods::*method)());
int invokeit2(VDerived &o, int (VDerived::*method)());

int test(VirtMethods &o)
{
    return invokeit2(o, &VirtMethods::m);
}

int test(VDerived &o)
{
    return invokeit2(o, &VDerived::m);
}

Без /vmg генерируется следующий код, который просто проходит простойуказатель на функцию в регистре для класса только с виртуальными методами.С другой стороны, класс с виртуальным базовым классом требует намного больше данных в структуре, передаваемой в память.

o$ = 8
int test(VirtMethods &) PROC                  ; test, COMDAT
        lea     rdx, OFFSET FLAT:[thunk]:VirtMethods::`vcall'{0,{flat}}' }' ; VirtMethods::`vcall'{0}'
        jmp     int invokeit2(VirtMethods &,int (__cdecl VirtMethods::*)(void)) ; invokeit2
int test(VirtMethods &) ENDP                  ; test

$T1 = 32
$T2 = 32
o$ = 64
int test(VDerived &) PROC               ; test, COMDAT
$LN4:
        sub     rsp, 56                             ; 00000038H
        and     DWORD PTR $T2[rsp+8], 0
        lea     rax, OFFSET FLAT:[thunk]:VDerived::`vcall'{0,{flat}}' }'     ; VDerived::`vcall'{0}'
        mov     QWORD PTR $T2[rsp], rax
        lea     rdx, QWORD PTR $T1[rsp]
        mov     DWORD PTR $T2[rsp+12], 4
        movaps  xmm0, XMMWORD PTR $T2[rsp]
        movdqa  XMMWORD PTR $T1[rsp], xmm0
        call    int invokeit2(VDerived &,int (__cdecl VDerived::*)(void)) ; invokeit2
        add     rsp, 56                             ; 00000038H
        ret     0
int test(VDerived &) ENDP               ; test


[thunk]:VDerived::`vcall'{0,{flat}}' }' PROC                         ; VDerived::`vcall'{0}', COMDAT
        mov     rax, QWORD PTR [rcx]
        jmp     QWORD PTR [rax]
[thunk]:VDerived::`vcall'{0,{flat}}' }' ENDP                         ; VDerived::`vcall'{0}'

[thunk]:VirtMethods::`vcall'{0,{flat}}' }' PROC                            ; VirtMethods::`vcall'{0}', COMDAT
        mov     rax, QWORD PTR [rcx]
        jmp     QWORD PTR [rax]
[thunk]:VirtMethods::`vcall'{0,{flat}}' }' ENDP    

С другой стороны, с / vmg код для простого класса выглядит совершенно иначе:

$T1 = 32
$T2 = 64
o$ = 112
int test(VirtMethods &) PROC                  ; test, COMDAT
$LN4:
        sub     rsp, 104                      ; 00000068H
        lea     rax, OFFSET FLAT:[thunk]:VirtMethods::`vcall'{0,{flat}}' }' ; VirtMethods::`vcall'{0}'
        mov     QWORD PTR $T1[rsp], rax
        lea     rdx, QWORD PTR $T2[rsp]
        xor     eax, eax
        mov     QWORD PTR $T1[rsp+8], rax
        movups  xmm0, XMMWORD PTR $T1[rsp]
        mov     DWORD PTR $T1[rsp+16], eax
        movsd   xmm1, QWORD PTR $T1[rsp+16]
        movaps  XMMWORD PTR $T2[rsp], xmm0
        movsd   QWORD PTR $T2[rsp+16], xmm1
        call    int invokeit2(VirtMethods &,int (__cdecl VirtMethods::*)(void)) ; invokeit2
        add     rsp, 104                      ; 00000068H
        ret     0
int test(VirtMethods &) ENDP                  ; test
...