无论第一个声明如何,问题都存在,如果您注释掉第一个声明并TriggerIssue()
在程序中调用,问题仍然存在。
这是由在 ' 退出时调用' 析构函数时cl
生成要调用的代码引起的,而不是由两个声明之间的任何怪癖或交互引起的。如果您不注释掉非声明,它显示的原因是另一个声明告诉编译器您希望它为函数生成一个符号,以便它可以导出到其他模块,这会阻止它实际内联代码,而不是强制它生成一个正常的函数。生成函数的主体时,它以对 的隐式调用结束,这是问题的根源。SomeFunc()
Foo
TriggerIssue()
inline
~Foo()
但是,如果非inline
声明被注释掉,编译器会乐于将代码视为内联代码,并且仅在您实际调用它时才生成它;由于您的测试程序实际上并没有调用TriggerIssue()
,因此永远不会生成代码,也永远不会调用该代码~Foo()
;由于析构函数也是inline
,这允许编译器完全忽略它而不为它生成代码。但是,如果您在测试程序中插入调用TriggerIssue()
,您将看到完全相同的错误消息。
测试#1:两个声明都存在。
我直接编译了您的代码,将输出传送到日志文件。
cl MyTest.cpp > MyTest.log
生成的日志文件是:
MyTest.cpp
Microsoft (R) Incremental Linker Version 10.00.40219.01
Copyright (C) Microsoft Corporation. All rights reserved.
/out:MyTest.exe
MyTest.obj
MyTest.obj : error LNK2019: unresolved external symbol "__declspec(dllimport) void __cdecl SomeFunc(void)" (__imp_?SomeFunc@@YAXXZ) referenced in function "public: __thiscall Foo::~Foo(void)" (??1Foo@@QAE@XZ)
MyTest.exe : fatal error LNK1120: 1 unresolved externals
测试 2:非inline
声明被注释掉,TriggerIssue()
被调用main()
。
我对您的代码进行了一些更改:
// A2.h was unchanged.
// -----
// A1.h:
#include "A2.h"
//extern "C" void TriggerIssue(); // <-- This!
extern "C" inline void TriggerIssue()
{
Foo f;
}
// -----
// MyTest.cpp
#include "A1.h"
int main()
{
TriggerIssue();
return 0;
}
我再次编译代码并将结果通过管道传输到日志文件,使用与以前相同的命令行:
MyTest.cpp
Microsoft (R) Incremental Linker Version 10.00.40219.01
Copyright (C) Microsoft Corporation. All rights reserved.
/out:MyTest.exe
MyTest.obj
MyTest.obj : error LNK2019: unresolved external symbol "__declspec(dllimport) void __cdecl SomeFunc(void)" (__imp_?SomeFunc@@YAXXZ) referenced in function "public: __thiscall Foo::~Foo(void)" (??1Foo@@QAE@XZ)
MyTest.exe : fatal error LNK1120: 1 unresolved externals
请注意,如果您愿意,两次编译代码的尝试都会导致相同的链接器错误,对于相同的符号,在相同的函数中。这是因为问题实际上是由 引起的~Foo()
,而不是TriggerIssue()
;第一个声明TriggerIssue()
仅仅暴露了它,通过强制编译器为~Foo()
.
[请注意,根据我的经验,Visual C++ 将尝试尽可能安全地优化一个类,并拒绝为其inline
成员函数生成代码,如果该类未被实际使用。这就是为什么阻止调用函数TriggerIssue()
的原因:由于没有被调用,编译器可以自由地对其进行完全优化,这允许它完全优化,包括对.]的调用inline
SomeFunc()
TriggerIssue()
~Foo()
SomeFunc()
测试 3:提供外部符号。
使用与测试 2中相同A2.h
的 ,A1.h
和,我制作了一个简单的 DLL 导出符号,然后告诉编译器链接它:MyTest.cpp
// SomeLib.cpp
void __declspec(dllexport) SomeFunc() {}
编译:
cl SomeLib.cpp /LD
这将创建SomeLib.dll
和SomeLib.lib
以及编译器和链接器使用的其他一些文件。然后,您可以使用以下命令编译示例代码:
cl MyTest.cpp SomeLib.lib > MyTest.log
这会产生一个可执行文件和以下日志:
MyTest.cpp
Microsoft (R) Incremental Linker Version 10.00.40219.01
Copyright (C) Microsoft Corporation. All rights reserved.
/out:MyTest.exe
MyTest.obj
SomeLib.lib
解决方案:
要解决此问题,您需要向编译器或链接器提供与SomeFunc()
导入 DLL 对应的库;如果提供给编译器,它将直接传递给链接器。例如,如果SomeFunc()
包含在 中,您将使用以下命令进行编译:SomeFuncLib.dll
cl MyTest.cpp SomeFuncLib.lib
为了说明差异,我成功编译了两次测试代码(每次都稍作修改),并用于dumpbin /symbols
生成的目标文件。
dumpbin/symbols MyTest.obj > MyTest.txt
示例 1:未inline
声明已注释掉,TriggerIssue()
未调用。
该目标文件是通过注释掉TriggerIssue()
示例代码中的第一个声明而生成的,但不进行任何修改A2.h
或MyTest.cpp
以任何方式进行修改。 TriggerIssue()
是inline
,而不是调用。
如果未调用该函数,并且允许编译器调用该函数inline
,则只会生成以下内容:
COFF SYMBOL TABLE
000 00AB9D1B ABS notype Static | @comp.id
001 00000001 ABS notype Static | @feat.00
002 00000000 SECT1 notype Static | .drectve
Section length 2F, #relocs 0, #linenums 0, checksum 0
004 00000000 SECT2 notype Static | .debug$S
Section length 68, #relocs 0, #linenums 0, checksum 0
006 00000000 SECT3 notype Static | .text
Section length 7, #relocs 0, #linenums 0, checksum 96F779C9
008 00000000 SECT3 notype () External | _main
请注意,如果您愿意,生成的唯一函数符号是 for main()
(这是隐含的extern "C"
,因此它可以链接到 CRT)。
示例 2:上述测试 3 的结果。
此目标文件是成功编译上述测试 3的结果。 TriggerIssue()
是inline
, 并调用main()
.
COFF SYMBOL TABLE
000 00AB9D1B ABS notype Static | @comp.id
001 00000001 ABS notype Static | @feat.00
002 00000000 SECT1 notype Static | .drectve
Section length 2F, #relocs 0, #linenums 0, checksum 0
004 00000000 SECT2 notype Static | .debug$S
Section length 68, #relocs 0, #linenums 0, checksum 0
006 00000000 SECT3 notype Static | .text
Section length C, #relocs 1, #linenums 0, checksum 226120D7
008 00000000 SECT3 notype () External | _main
009 00000000 SECT4 notype Static | .text
Section length 18, #relocs 2, #linenums 0, checksum 6CFCDEF, selection 2 (pick any)
00B 00000000 SECT4 notype () External | _TriggerIssue
00C 00000000 SECT5 notype Static | .text
Section length E, #relocs 0, #linenums 0, checksum 4DE4BFBE, selection 2 (pick any)
00E 00000000 SECT5 notype () External | ??0Foo@@QAE@XZ (public: __thiscall Foo::Foo(void))
00F 00000000 SECT6 notype Static | .text
Section length 11, #relocs 1, #linenums 0, checksum DE24CF19, selection 2 (pick any)
011 00000000 SECT6 notype () External | ??1Foo@@QAE@XZ (public: __thiscall Foo::~Foo(void))
012 00000000 UNDEF notype External | __imp_?SomeFunc@@YAXXZ (__declspec(dllimport) void __cdecl SomeFunc(void))
通过比较这两个符号表,我们可以看到when TriggerIssue()
is inline
d,如果被调用会生成以下四个符号,如果不被调用则省略:
_TriggerIssue
( extern "C" void TriggerIssue()
)
??0Foo@@QAE@XZ
( public: __thiscall Foo::Foo(void)
)
??1Foo@@QAE@XZ
( public: __thiscall Foo::~Foo(void)
)
__imp_?SomeFunc@@YAXXZ
( __declspec(dllimport) void __cdecl SomeFunc(void)
)
如果未生成符号 for SomeFunc()
,则链接器不需要链接它,无论它是否已声明。
所以,总结一下:
- 问题是由调用引起的,当链接器没有任何链接调用时。
~Foo()
SomeFunc()
SomeFunc()
- 该问题通过创建 , 的实例暴露出来,并且将显示如果是非(通过第一个声明)或调用时。
TriggerIssue()
Foo
TriggerIssue()
inline
inline
- 如果您注释掉的第一个声明并且实际上没有调用它,那么问题就隐藏了。
TriggerIssue()
由于您希望该函数被内联,并且实际上并未被调用,cl
因此可以自由地对其进行完全优化。优化TriggerIssue()
出来还可以优化Foo
的inline
成员函数,从而防止~Foo()
生成。这反过来又可以防止链接器抱怨SomeFunc()
析构函数中的调用,因为调用的代码SomeFunc()
从未生成。
甚至更短:
- 的第一个声明
TriggerIssue()
间接阻止编译器优化对SomeFunc()
. 如果您注释掉该声明,编译器可以自由地优化TriggerIssue()
和~Foo()
完全退出,这反过来会阻止编译器生成对 的调用SomeFunc()
,从而允许链接器完全忽略它。
要修复它,您需要提供一个库,该库link
可用于生成正确的代码以SomeFunc()
从相应的 DLL 导入。
编辑:正如user657267 在评论中指出的那样TriggerIssue()
,暴露问题的第一个声明的特定部分是extern "C"
. 从问题的示例程序开始:
- 如果
extern "C"
从两个声明中完全删除 ,并且没有其他任何更改,那么编译器将在编译代码时优化TriggerIssue()
(并通过扩展,~Foo()
),生成与上面示例 1中的符号表相同的符号表。
- 如果
"C"
从两个声明中删除 ,但函数保留为,并且没有其他任何更改,则链接阶段将失败,生成与测试 1 和 2extern
中相同的日志文件。
这表明该extern
声明专门负责防止cl
优化问题代码,方法是强制编译器生成一个可以在其他模块中外部链接的符号。如果编译器不需要担心外部链接,它会优化TriggerIssue()
,并通过扩展~Foo()
,完全脱离完成的程序,从而无需链接到另一个模块的SomeFunc()
.