3

鉴于此代码:

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; 
}

我的测试.cpp

#include "A1.h"

int main()
{
  return 0;
}

请参阅此处了解问题的背景。

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

这似乎是由于 A1.h 中的 TriggerIssue 的无关(错误?)声明引起的。将其注释掉会导致链接器错误消失。

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

投票者注意事项:这不是关于如何修复未解决的外部符号错误的问题。因此,请停止投票以将其关闭为重复。我没有足够的信誉来删除一直显示在这篇文章顶部的那个链接,声称这个问题“可能有一个可能的答案”。

4

2 回答 2

3

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

这是由在 ' 退出时调用' 析构函数时cl生成要调用的代码引起的,而不是由两个声明之间的任何怪癖或交互引起的。如果您不注释掉非声明,它显示的原因是另一个声明告诉编译器您希望它为函数生成一个符号,以便它可以导出到其他模块,这会阻止它实际内联代码,而不是强制它生成一个正常的函数。生成函数的主体时,它以对 的隐式调用结束,这是问题的根源。SomeFunc()FooTriggerIssue()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()的原因:由于没有被调用,编译器可以自由地对其进行完全优化,这允许它完全优化,包括对.]的调用inlineSomeFunc()TriggerIssue()~Foo()SomeFunc()


测试 3:提供外部符号。

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

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

编译:

cl SomeLib.cpp /LD

这将创建SomeLib.dllSomeLib.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.hMyTest.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 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))

如果未生成符号 for SomeFunc(),则链接器不需要链接它,无论它是否已声明。



所以,总结一下:

  • 问题是由调用引起的,当链接器没有任何链接调用时。~Foo()SomeFunc()SomeFunc()
  • 该问题通过创建 , 的实例暴露出来,并且将显示如果是非(通过第一个声明)或调用时。TriggerIssue()FooTriggerIssue()inlineinline
  • 如果您注释掉的第一个声明并且实际上没有调用它,那么问题就隐藏了。TriggerIssue()由于您希望该函数被内联,并且实际上并未被调用,cl因此可以自由地对其进行完全优化。优化TriggerIssue()出来还可以优化Fooinline成员函数,从而防止~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().

于 2016-05-30T22:33:30.747 回答
2

SomeFunc在您的程序中使用了 ODR,因此必须提供定义,但您尚未提供定义(在此翻译单元中或通过在另一个翻译单元中链接)并且您的程序具有未定义的行为,不需要诊断™。

链接器给你一个错误的原因是因为编译器已经为TriggerIssue;生成了一个定义。根据额外声明的存在,行为会有所不同,这当然很奇怪,您希望它们至少具有相同的行为。除了 UB,编译器仍然可以自由选择:函数是这样的inline,因此您可以保证函数的任何和所有定义都是相同的,因此如果在链接时有任何重复符号,链接器可以简单地将它们丢弃。

于 2016-05-30T21:26:26.637 回答