为什么在指定extern "C" 时Visual Studio 无法给出未定义的引用错误?

Why does Visual Studio fail to give an undefined reference error when extern "C" is specified?

鉴于此代码:

A2.H

_declspec(dllimport) void SomeFunc();

struct Foo
{
  Foo();
  ~Foo();
};

inline Foo::Foo() { }

inline Foo::~Foo()
{
  SomeFunc();
}

A1.H

#include "A2.h"

extern "C" void TriggerIssue(); // <-- This!

extern "C" inline void TriggerIssue()
{
  Foo f; 
}

MyTest.cpp

#include "A1.h"

int main()
{
  return 0;
}

请参阅 here 了解问题的背景。

当 MyTest.cpp 被编译成可执行文件时,linker 抱怨 SomeFunc() 是一个未解析的外部文件。

这似乎是由于一个无关的(错误的?)声明引起的 A1.h 中的触发问题。注释掉会导致 linker 错误消失。

有人能告诉我这是怎么回事吗?我只想了解是什么具体导致编译器在存在和不存在该声明时表现不同。上面的代码片段是我尝试编写一个我 运行 的场景的最小可验证示例。请不要问我为什么它是这样写的。

投反对票者注意:这是不是关于如何修复未解决的外部符号错误的问题。所以请停止投票以将其作为重复项关闭。我没有足够的信誉来删除那个 link 一直出现在这个 post 的顶部声称这个问题 "may have a possible answer".

SomeFunc 在您的程序中是 ODR-used,因此定义必须可用,但您没有提供定义(无论是在此翻译单元中还是通过 linking 在另一个翻译单元中) 并且您的程序具有未定义的行为,不需要诊断™。

之所以linker给你报错是因为编译器生成了TriggerIssue的定义;根据额外声明的存在,行为会有所不同,这当然很好奇,您希望它们至少具有相同的行为。除了 UB,编译器仍然可以自由选择:函数是 inline 所以你保证函数的任何和所有定义都是相同的,所以如果在 link 时间有任何重复符号linker 可以简单地把它们扔掉。

无论第一个声明如何,该问题都会存在,如果您注释掉第一个声明并在程序中调用 TriggerIssue(),问题仍然存在。

这是由于 clTriggerIssue() 退出时调用 Foo 的析构函数时生成调用 SomeFunc() 的代码,而不是由任何怪癖或交互引起的在两个声明之间。如果您不注释掉非 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

请注意,如果您愿意的话,两次编译代码的尝试都会导致相同的 linker 错误,对于相同的符号,在相同的函数中。这是因为问题实际上是由~Foo()引起的,而不是TriggerIssue()TriggerIssue() 的第一个声明只是通过强制编译器为 ~Foo().

生成代码来暴露它

[请注意,根据我的经验,Visual C++ 将尝试尽可能安全地优化 class,并拒绝为其 inline 成员函数生成代码,如果 class 实际上并没有被使用。这就是为什么将 TriggerIssue() 设为 inline 函数会阻止 SomeFunc() 被调用的原因:由于 TriggerIssue() 未被调用,编译器可以自由地完全优化它,从而允许它完全优化 ~Foo(),包括对 SomeFunc().]

的调用

测试 3:提供了外部符号。

使用与 测试 2 中相同的 A2.hA1.hMyTest.cpp,我制作了一个导出符号的简单 DLL , 然后告诉编译器用它 link:

// SomeLib.cpp
void __declspec(dllexport) SomeFunc() {}

编译:

cl SomeLib.cpp /LD

这将创建 SomeLib.dllSomeLib.lib,以及编译器和 link 用户使用的一些其他文件。然后你可以编译你的示例代码:

cl MyTest.cpp SomeLib.lib > MyTest.log

这会产生一个 executable 和以下日志:

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 

解决方法:

要解决此问题,您需要为编译器或 linker 提供与导入的 DLL SomeFunc() 对应的库;如果提供给编译器,它将直接传递给 linker。例如,如果 SomeFunc() 包含在 SomeFuncLib.dll 中,您将编译:

cl MyTest.cpp SomeFuncLib.lib

为了说明差异,我成功编译了两次测试代码(每次都稍作修改),并对生成的目标文件使用了 dumpbin /symbols

dumpbin/symbols MyTest.obj > MyTest.txt

示例 1:非 inline 声明被注释掉,TriggerIssue() 未被调用。

此目标文件是通过注释掉示例代码中 TriggerIssue() 的第一个声明而生成的,但未以任何方式修改 A2.hMyTest.cppTriggerIssue()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

请注意,如果您愿意的话,唯一生成的函数符号是 main()(隐式 extern "C",因此它可以 link 到 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))

通过比较这两个符号table可以看出,当TriggerIssue()inlined时,如果调用则生成以下四个符号,如果调用则省略它不是:

  • _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))

如果 SomeFunc() 的符号没有生成,那么 linker 不需要 link 它,不管它是否被声明。



所以,总结一下:

  • 问题是由 ~Foo() 调用 SomeFunc() 引起的 ,当时 linker 没有任何 SomeFunc()到 link 调用到.
  • 问题是由 TriggerIssue() 创建 Foo 的实例 暴露的 ,并且如果 TriggerIssue() 是非inline(通过第一个声明)或在 inline.
  • 时调用
  • 如果您注释掉 TriggerIssue() 的第一个声明并且实际上没有调用它,那么问题是 隐藏。由于您希望内联函数,但实际上并未调用它,因此 cl 可以自由地完全优化它。优化 TriggerIssue() 也让它优化了 Fooinline 成员函数,从而防止生成 ~Foo()。反过来,这可以防止 linker 抱怨析构函数中的 SomeFunc() 调用,因为从未生成调用 SomeFunc() 的代码。

或更短:

  • TriggerIssue() 的第一个声明间接阻止了编译器优化对 SomeFunc() 的调用。如果您注释掉该声明,编译器可以完全优化 TriggerIssue()~Foo(),这反过来会阻止编译器生成对 SomeFunc() 的调用,从而允许 linker 完全忽略它。

要修复它,您需要提供一个库,link 可以使用它来生成正确的代码以从适当的 DLL 导入 SomeFunc()



编辑:与一样,TriggerIssue()第一个声明暴露问题的具体部分是extern "C"。从问题的示例程序开始:

  • 如果 extern "C" 从两个声明中完全删除,并且没有其他任何更改,那么编译器将在编译时优化 TriggerIssue()(并扩展为 ~Foo())代码,生成一个符号 table 与上面 示例 1 中的符号相同。
  • 如果从两个声明中删除 "C" 但函数保留为 extern,并且没有其他任何更改,则 linking 阶段将失败,产生相同的结果测试 1 和 2.
  • 中的日志文件

这表明 extern 声明专门负责防止 cl 优化问题代码,通过强制编译器生成可以在外部 linked 的符号在其他模块中。如果编译器不需要担心外部 linkage,它将优化 TriggerIssue(),并通过扩展 ~Foo(),完全脱离完成的程序,从而消除对 link 到另一个模块的 SomeFunc().