问题
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 inline
d, 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()
callingSomeFunc()
, when the linker doesn't have anySomeFunc()
to link the call to. - The problem is exposed by
TriggerIssue()
creating an instance ofFoo
, and will show up either ifTriggerIssue()
is made non-inline
(by the first declaration) or called wheninline
. - 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. OptimisingTriggerIssue()
out also lets it optimiseFoo
'sinline
member functions out, which prevents~Foo()
from being generated. This, in turn, prevents the linker from complaining about theSomeFunc()
call in the destructor, since the code to callSomeFunc()
was never generated.
Or even shorter:
- The first declaration of
TriggerIssue()
indirectly prevents the compiler from optimising out the call toSomeFunc()
. If you comment out that declaration, the compiler is free to optimiseTriggerIssue()
and~Foo()
out entirely, which in turn stops the compiler from generating a call toSomeFunc()
, 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 optimiseTriggerIssue()
(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 asextern
, 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