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

狂风中的少年 提交于 2019-12-10 20:08:03

问题


Given this code:

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

Please see here for a background to the issue.

When MyTest.cpp is compiled into an executable, the linker complains that SomeFunc() is an unresolved external.

This seems to be caused because of an extraneous (erroneous?) declaration of TriggerIssue in A1.h. Commenting that out causes the linker error to go away.

Can someone tell me what's going on here? I just want to understand what specifically causes the compiler to behave differently in the presence and absence of that declaration. The snippet above is my attempt to write a minimally verifiable example of a scenario I am running into. Please don't ask me why its written the way it is.

Note to downvoters: This is NOT a question about how to fix unresolved external symbol errors. So please STOP voting to close this as duplicate. I don't have enough cred to remove that link that keeps showing up at the top of this post claiming this question "may have a possible answer".


回答1:


The issue is present regardless of the first declaration, and will still be present if you comment out the first declaration and call TriggerIssue() in your program.

It's caused by cl generating the code to call SomeFunc() when it calls Foo's destructor upon TriggerIssue()'s exit, not by any quirk or interaction between the two declarations. The reason it shows up if you don't comment out the non-inline declaration is that the other declaration tells the compiler that you want it to generate a symbol for the function so it can be exported to other modules, which prevents it from actually inlining the code, instead forcing it to generate a normal function. When the function's body is generated, it ends with an implicit call to ~Foo(), which is the source of the issue.

If the non-inline declaration is commented out, however, the compiler will merrily treat the code as inline, and only generate it if you actually call it; since your test program doesn't actually call TriggerIssue(), the code is never generated, and ~Foo() is never called; since the destructor is also inline, this allows the compiler to ignore it entirely and not generate code for it. If you do insert a call to TriggerIssue() in your test program, however, you'll see the exact same error message.


Test #1: Both declarations present.

I compiled your code directly, piping the output to a log file.

cl MyTest.cpp > MyTest.log

The resulting log file was:

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

Test 2: Non-inline declaration commented out, TriggerIssue() called in main().

I made a couple changes to your code:

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

I again compiled the code and piped the results to a log file, using the same command line as before:

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

Note, if you will, that both attempts to compile the code resulted in the same linker error, for the same symbol, in the same function. This is because the problem is actually caused by ~Foo(), not TriggerIssue(); the first declaration of TriggerIssue() merely exposed it, by forcing the compiler to generate code for ~Foo().

[Note that in my experience, Visual C++ will attempt to optimise a class out as much as is safely possible, and refuse to generate code for its inline member functions, if the class isn't actually used. This is why making TriggerIssue() an inline function prevented SomeFunc() from being called: Since TriggerIssue() wasn't called, the compiler was free to optimise it out entirely, which allowed it to optimise ~Foo() out entirely, including the call to SomeFunc().]


Test 3: External symbol provided.

Using the same A2.h, A1.h, and MyTest.cpp as in Test 2, I made a simple DLL that exports the symbol, then told the compiler to link with it:

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

Compile with:

cl SomeLib.cpp /LD

This creates SomeLib.dll and SomeLib.lib, along with some other files the compiler & linker use. You can then compile your example code with:

cl MyTest.cpp SomeLib.lib > MyTest.log

This results in an executable, and the following 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 

The solution:

To resolve this issue, you need to provide either the compiler or the linker with the library corresponding to the DLL SomeFunc() is imported from; if given to the compiler, it'll be passed directly to the linker. If SomeFunc() is contained in SomeFuncLib.dll, for example, you would compile with:

cl MyTest.cpp SomeFuncLib.lib

To illustrate the difference, I compiled the test code successfully twice (with slight modifications each time), and used dumpbin /symbols on the resulting object files.

dumpbin/symbols MyTest.obj > MyTest.txt

Example 1: Non-inline declaration commented out, TriggerIssue() not called.

This object file was generated by commenting out the first declaration of TriggerIssue() in your example code, but not modifying either A2.h or MyTest.cpp in any way. TriggerIssue() is inline, and not called.

If the function isn't called, and the compiler is allowed to inline it, then only the following will be generated:

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

Note, if you will, that the only function symbol generated was for main() (which is implicitly extern "C" so it can link to the CRT).

Example 2: Result from Test 3 above.

This object file was generated as a result of successfully compiling Test 3 above. TriggerIssue() is inline, and called in 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))

By comparing these two symbol tables, we can see that when TriggerIssue() is inlined, the following four symbols will by generated if it is called, or omitted if it isn't:

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

If the symbol for SomeFunc() isn't generated, the linker doesn't need to link it, regardless of whether it was declared or not.



So, to summarise:

  • The problem is caused by ~Foo() calling SomeFunc(), when the linker doesn't have any SomeFunc() to link the call to.
  • The problem is exposed by TriggerIssue() creating an instance of Foo, and will show up either if TriggerIssue() is made non-inline (by the first declaration) or called when inline.
  • The problem is hidden if you comment out TriggerIssue()'s first declaraction and don't actually call it. Since you want the function to be inlined, and it isn't actually called, cl is free to optimise it out entirely. Optimising TriggerIssue() out also lets it optimise Foo's inline member functions out, which prevents ~Foo() from being generated. This, in turn, prevents the linker from complaining about the SomeFunc() call in the destructor, since the code to call SomeFunc() was never generated.

Or even shorter:

  • The first declaration of TriggerIssue() indirectly prevents the compiler from optimising out the call to SomeFunc(). If you comment out that declaration, the compiler is free to optimise TriggerIssue() and ~Foo() out entirely, which in turn stops the compiler from generating a call to SomeFunc(), allowing the linker to ignore it entirely.

To fix it, you need to provide a library that link can use to generate the proper code to import SomeFunc() from the appropriate DLL.



Edit: As user657267 pointed out in the comments, the specific part of TriggerIssue()'s first declaration that exposes the issue is the extern "C". Starting with the question's example program:

  • If the extern "C" is removed entirely from both declarations, and nothing else is changed, then the compiler will optimise TriggerIssue() (and by extension, ~Foo()) out as it compiles the code, generating a symbol table identical to the one in Example 1 above.
  • If the "C" is removed from both declarations but the function is left as extern, and nothing else is changed, then the linking stage will fail, producing the same log file as in Tests 1 & 2.

This suggests that the extern declaration is specifically responsible for preventing cl from optimising the problem code out, by forcing the compiler to generate a symbol that can be externally linked in other modules. If the compiler doesn't need to worry about external linkage, it will optimise TriggerIssue(), and by extension ~Foo(), out of the finished program entirely, thus removing the need to link to another module's SomeFunc().




回答2:


SomeFunc is ODR-used in your program, so a definition must be available, but you haven't provided one (either in this translation unit or by linking in another) and your program has undefined behavior, no diagnostic required™.

The reason why the linker gives you an error is because the compiler has generated a definition for TriggerIssue; it's certainly curious that the behaviour is different depending on the presence of the extra declaration, you'd expect them to at least have the same behavior. UB aside, the compiler is still free to choose: the function is inline so you're guaranteeing that any and all definitions of the function will be identical, so if there are any dupe symbols at link time the linker can simply throw them out.



来源:https://stackoverflow.com/questions/37515821/why-does-visual-studio-fail-to-give-an-undefined-reference-error-when-extern-c

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!