在将类定义移动到另一个模块之后,我一直在调查一个奇怪的崩溃,并得出结论,编译器对如何定义成员函数的指针感到困惑。
我不能包含整个代码,因为它是一个庞大的程序,我不能在一个较小的例子上重现它。
编辑:我设法在一个小例子上重现崩溃,所以我正在编辑整个问题以包含新的代码和程序集。
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]
第一个代码在调用指令上崩溃。它试图在&action+8
读取一个dword值,该值从未被初始化,并导致call
指令崩溃。
我发现了一个related bug from half a year ago,但它本应在15.9修正,当时我目前在15.9.7。
这是VS2017中的另一个错误,还是我在使用成员函数指针和前向声明做了一些无意识的事情?
我几乎可以肯定,问题可以通过选项/vmg
来解决。
否则,编译器将根据类定义优化成员指针的表示。具有多个基类的类需要不同于没有它们的类的成员指针,而具有虚拟基类的类可能需要更复杂的类。
如果没有/vmg
,编译器将生成不同的代码,具体取决于它是否已经看到IStatesManager
的完整定义,我认为这个名称是虚拟方法的接口。
使用此类的所有模块也必须使用/ vmg选项进行编译,因此传入正确类型的成员指针。
或者,您可以在IStatemanager
标头中包含ControlNode
的标头,但我认为前向声明是故意用于减少依赖关系。
编辑:编译器still optimizes方法指针调用代码时它知道类定义,并因此可以排除复杂的虚拟派生情况,如注释中所述,重要的区别在于方法指针的初始化,这是保证的与/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