3

在过去的一个小时里,我一直在寻找这个问题的答案,但找不到有效的解决方案。我正在尝试使用函数指针来调用特定对象的非静态成员函数。我的代码编译得很好,但是在运行时我得到一个讨厌的运行时异常,它说:

运行时检查失败 #0 - ESP 的值未在函数调用中正确保存。这通常是调用使用一种调用约定声明的函数和使用另一种调用约定声明的函数指针的结果。

很多网站说要在方法头中指定调用约定,所以我__cdecl在它前面加了。但是,我的代码在更改后遇到了相同的运行时异常(我也尝试使用其他调用约定)。我不确定为什么我必须首先指定 cdecl,因为我的项目设置设置为 cdecl。我正在使用一些外部库,但在我添加这个函数指针之前这些库工作正常。

我正在关注这个:https ://stackoverflow.com/a/151449

我的代码:

#pragma once

class B;
typedef void (B::*ReceiverFunction)();

class A
{
public:
    A();
    ~A();
    void addEventListener(ReceiverFunction receiverFunction);
};

A.cpp

#include "A.h"

A::A(){}
A::~A(){}
void A::addEventListener(ReceiverFunction receiverFunction)
{
    //Do nothing
}

溴化氢

#pragma once

#include <iostream>
#include "A.h"

class B
{
public:
    B();
    ~B();
    void testFunction();
    void setA(A* a);
    void addEvent();

private:
    A* a;

};

B.cpp

#include "B.h"

B::B(){}
B::~B(){}

void B::setA(A* a)
{
    this->a = a;
}
void B::addEvent()
{
    a->addEventListener(&B::testFunction); //This is the offending line for the runtime exception
}
void B::testFunction()
{
    //Nothing here
}

主文件

#include "A.h"
#include "B.h"

int main()
{
    A* a = new A();
    B* b = new B();
    b->setA(a);
    b->addEvent();
}

我正在使用 Visual Studio 2010 运行,但我希望我的代码能够在其他平台上运行,并且只需进行最少的更改。

4

3 回答 3

8

这是一个已知问题,必要的成分是使用不完整类的成员指针声明并将其用于不同的翻译单元。MSVC 编译器中的一种优化,它根据继承对成员指针使用不同的内部表示。

解决方法是编译/vmg显式声明继承

class __single_inheritance B;
typedef void (B::*ReceiverFunction)();
于 2011-12-30T10:50:33.120 回答
4

似乎没有多少人重现了这个问题,我将首先在这里展示VS2010在这段代码上的行为。(调试版本,32 位操作系统)

问题出在B::addEven()和中A::addEventListener()。为了给我一个参考点来检查这个ESP值,两个额外的语句被添加到B::addEven().

// in B.cpp, where B is complete
void B::addEvent()
{
00411580  push        ebp  
00411581  mov         ebp,esp  
00411583  sub         esp,0D8h  
00411589  push        ebx  
0041158A  push        esi  
0041158B  push        edi  
0041158C  push        ecx  
0041158D  lea         edi,[ebp-0D8h]  
00411593  mov         ecx,36h  
00411598  mov         eax,0CCCCCCCCh  
0041159D  rep stos    dword ptr es:[edi]  
0041159F  pop         ecx  
004115A0  mov         dword ptr [ebp-8],ecx  
    int i = sizeof(ReceiverFunction); // added, sizeof(ReceiverFunction) is 4
004115A3  mov         dword ptr [i],4  
    a->addEventListener(&B::testFunction); //This is the offending line for the runtime exception
004115AA  push        offset B::testFunction (411041h)  
004115AF  mov         eax,dword ptr [this]  
004115B2  mov         ecx,dword ptr [eax]  
004115B4  call        A::addEventListener (4111D6h)  
    i = 5;            // added
004115B9  mov         dword ptr [i],5  
}
004115C0  pop         edi  
004115C1  pop         esi  
004115C2  pop         ebx  
004115C3  add         esp,0D8h  
004115C9  cmp         ebp,esp  
004115CB  call        @ILT+330(__RTC_CheckEsp) (41114Fh)  
004115D0  mov         esp,ebp  
004115D2  pop         ebp  
004115D3  ret  

// In A.cpp, where B is not complete
void A::addEventListener(ReceiverFunction receiverFunction)
{
00411470  push        ebp  
00411471  mov         ebp,esp  
00411473  sub         esp,0D8h  
00411479  push        ebx  
0041147A  push        esi  
0041147B  push        edi  
0041147C  push        ecx  
0041147D  lea         edi,[ebp-0D8h]  
00411483  mov         ecx,36h  
00411488  mov         eax,0CCCCCCCCh  
0041148D  rep stos    dword ptr es:[edi]  
0041148F  pop         ecx  
00411490  mov         dword ptr [ebp-8],ecx  
    int i = sizeof(receiverFunction);  // added, sizeof(receiverFunction) is 10h
00411493  mov         dword ptr [i],10h  
    //Do nothing
}
0041149A  pop         edi  
0041149B  pop         esi  
0041149C  pop         ebx  
0041149D  mov         esp,ebp  
0041149F  pop         ebp  
004114A0  ret         10h  

A:: addEventListener()用于ret 10h清除堆栈,但只有 4 个字节被压​​入堆栈(push offset B::testFunction),这导致堆栈帧被破坏。

似乎取决于是否B完整,sizeof(void B::*func())在VS2010中会发生变化。在 OP 的代码中,在 A.cppB中是不完整的,大小为10h. 在调用站点 B.cpp 中,当B已经完成时,大小变为04h. (这可以通过sizeof(ReceiverFunction)如上面的代码所示进行检查)。这导致在调用站点和实际代码中A::addEventListener(),扩充/参数的大小不一样,从而导致堆栈损坏。

我更改了包含顺序以确保B每个翻译单元都完整,并且运行时错误消失。

这应该是VS2010的错误...


编译器命令行:

/ZI /nologo /W3 /WX- /Od /Oy- /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /Gm /EHsc /RTC1 /GS /fp:precise /Zc:wchar_t /Zc:forScope /Fp"Debug\test.pch" /Fa"Debug\" /Fo"Debug\" /Fd"Debug\vc100.pdb" /Gd /analyze- /errorReport:queue 

链接器命令行:

/OUT:"...\test.exe" /INCREMENTAL /NOLOGO "kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" "comdlg32.lib" "advapi32.lib" "shell32.lib" "ole32.lib" "oleaut32.lib" "uuid.lib" "odbc32.lib" "odbccp32.lib" /MANIFEST /ManifestFile:"Debug\test.exe.intermediate.manifest" /ALLOWISOLATION /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /DEBUG /PDB:"...\test.pdb" /SUBSYSTEM:CONSOLE /PGD:"...\test.pgd" /TLBID:1 /DYNAMICBASE /NXCOMPAT /MACHINE:X86 /ERRORREPORT:QUEUE 

我在命令行中隐藏了一些路径。

于 2011-12-30T09:54:42.187 回答
1

使用 /vmg 作为编译器选项解决了这个问题。

但是,我决定改用委托库(http://www.codeproject.com/KB/cpp/ImpossiblyFastCppDelegate.aspx),效果很好!

于 2011-12-31T07:39:27.350 回答