c++ - 指定extern “C”时,为什么Visual Studio无法给出 undefined reference 错误?

标签 c++ visual-c++ inline extern-c

给出以下代码:

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编译为可执行文件时,链接程序会抱怨SomeFunc()是无法解析的外部文件。

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

有人可以告诉我这是怎么回事吗?我只想了解到底是什么导致编译器在有和没有该声明的情况下表现不同。上面的代码段是我试图写出我所遇到的场景的最小可验证示例。请不要问我为什么这样写。

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

最佳答案

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

这是由于clSomeFunc()退出时调用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.hA1.hMyTest.cpp,我制作了一个简单的DLL来导出符号,然后告诉编译器与其链接:
// 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 

解决方案:

要解决此问题,您需要为编译器或链接器提供与从其中导入DLL SomeFunc()对应的库;如果提供给编译器,它将直接传递给链接器。例如,如果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",因此可以链接到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))

通过比较这两个符号表,我们可以看到,当TriggerIssue()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))

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



    总结一下:
  • 问题是由链接器没有任何~Foo()链接到该调用时,由SomeFunc()调用SomeFunc()引起的。
  • 通过TriggerIssue()创建Foo的实例可以解决该问题,并且如果将TriggerIssue()设置为非inline(通过第一个声明)或在inline时调用,则会显示此问题。
  • 如果您注释掉TriggerIssue()的第一个声明而不实际调用它,则问题将隐藏。由于您希望内联函数,而实际上并未调用它,因此cl可以自由地对其进行完全优化。通过优化TriggerIssue()输出,还可以优化Fooinline成员函数,从而阻止了~Foo()的生成。反过来,这可以防止链接器抱怨析构函数中的SomeFunc()调用,因为从未生成过调用SomeFunc()的代码。

  • 甚至更短:
  • TriggerIssue()的第一个声明间接地阻止了编译器优化对SomeFunc()的调用。如果您注释掉该声明,则编译器可以自由地完全优化TriggerIssue()~Foo(),从而阻止编译器生成对SomeFunc()的调用,从而允许链接程序完全忽略它。

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



    编辑:与user657267 pointed out in the comments一样,TriggerIssue()的第一个声明的特定部分公开的问题是extern "C"。从问题的示例程序开始:
  • 如果完全从两个声明中都删除了extern "C",并且未进行任何其他更改,则编译器将在编译代码时优化TriggerIssue()(并扩展为~Foo()),从而生成与中的符号表相同的符号表示例1 以上。
  • 如果从两个声明中都删除了"C",但该函数保留为extern,并且未进行其他任何更改,则链接阶段将失败,并生成与测试1和2 中相同的日志文件。

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

    关于c++ - 指定extern “C”时,为什么Visual Studio无法给出 undefined reference 错误?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37515821/

    相关文章:

    c++ - type 的值不能用于初始化 type 的实体

    c# - 从 C++ 库方法返回的 "char*"获取 C# 字符串?

    visual-c++ - 什么是 vs2012 cl.exe 相当于 gcc -std=c++11?

    html - 电子邮件模板中的内联定位

    c++ - 类中的内联函数

    c++ - 是否有用于 SDL 的 C++ 包装器/绑定(bind)?

    c++ - 使用 Ubuntu 在 C/C++ 中的 UDP 套接字发送限制

    c++ - 将输入层 reshape 为单 channel 和 multimap 像

    http - 如何重新组装tcp段?

    c++ - 内联函数性能