问题

demo

考虑下面这样的一段代码:

Foo.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma once
#ifndef TEST3_FOO_H
#define TEST3_FOO_H

#include <string>

class Bar;

typedef void (Bar::* MethodPtr)();

class Foo
{
public:
MethodPtr ptr;
std::string str1;
Foo(MethodPtr ptr, const std::string& str1);

};


#endif //TEST3_FOO_H

Foo.cpp

1
2
3
4
5
#include "Foo.h"

Foo::Foo(MethodPtr ptr, const std::string& str1) : ptr(ptr), str1(str1)
{}

Bar.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#pragma once
#ifndef TEST3_BAR_H
#define TEST3_BAR_H


class Bar
{
public:
void Method1();
void Method2();
void Method3();
void Exec();
};


#endif //TEST3_BAR_H

Bar.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "Bar.h"
#include "Foo.h"
#include <iostream>

const Foo foos[] = {
Foo(&Bar::Method1, "Method1"),
Foo(&Bar::Method2, "Method2"),
Foo(&Bar::Method3, "Method3"),
};

void Bar::Method1()
{
std::cout << "Bar::Method1()" << std::endl;
}

void Bar::Method2()
{
std::cout << "Bar::Method2()" << std::endl;
}

void Bar::Method3()
{
std::cout << "Bar::Method3()" << std::endl;
}

void Bar::Exec()
{
for (auto& foo : foos)
{
(this->*foo.ptr)();
}
}

main.cpp

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include "Bar.h"

int main()
{
std::cout << "Program executed successfully!" << std::endl;
Bar bar;
bar.Exec();
return 0;
}

问题

上面这段代码在G++与clang等平台均可以正常运行。但是若在msvc中运行,程序会出现异常并退出。 但是如果不使用前向声明(Forward declaration),就不会出现这样的问题。

原因

在其它编译器中,成员函数指针的size是固定的(2个指针长度)

但是在msvc中,成员函数指针的size会根据对应类继承关系的不同而不同。在前向声明中,编译器并不能判断对应类的继承关系,因而会错误地推断成员函数指针的size,并影响后续声明的类的size。进而在不同代码中访问对应内存会导致越界等异常。

这个错误非常隐蔽,即使在调试过程中没有产生异常,也应该修复这个错误

Workaround

对于msvc的项目,如果必须同时使用成员函数指针与前向声明,需要在前向声明语句中加入继承模式关键字来避免潜在的bug, 其中:

Keyword适用于
__single_inheritance单继承或无继承关系
__multiple_inheritance多重继承关系
__virtual_inheritance虚继承关系

例如,对于上面的示例,可以将Foo.h中的L7的前向声明改为以下行进行修复:

1
class __single_inheritance Bar;

这个关键字是Microsoft专用关键字,并不在C++规范内。如果您正在构建跨平台应用,需要使用不同的编译器编译同一份代码,建议通过宏进行区分。

Reference

详细的错误issue请参考:https://developercommunity.visualstudio.com/t/Unexpected-exception-when-using-a-combin/10017095?viewtype=all

Microsoft docs: https://docs.microsoft.com/en-us/cpp/cpp/inheritance-keywords?view=msvc-170