3

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".

Community
  • 1
  • 1
ForeverLearning
  • 6,352
  • 3
  • 27
  • 33
  • Possible duplicate of [What is an undefined reference/unresolved external symbol error and how do I fix it?](http://stackoverflow.com/questions/12573816/what-is-an-undefined-reference-unresolved-external-symbol-error-and-how-do-i-fix) – too honest for this site May 29 '16 at 23:38
  • @Olaf I know what an unresolved external error is. That is not what I am asking. Please re-read my post. – ForeverLearning May 29 '16 at 23:43
  • There's no definition of SomeFunc. Beyond that: Questions seeking debugging help ("why isn't this code working?") must include the desired behavior, a specific problem or error and the shortest code necessary to reproduce it in the question itself. Questions without a clear problem statement are not useful to other readers. See: How to create a Minimal, Complete, and Verifiable example. – xaxxon May 29 '16 at 23:57
  • 1
    Are you saying removing *only* the declaration, not the inline implementation, of `TriggerIssue`, subsides the linker error (And I would have to ask, since the inline decl is sufficient, I'm somewhat perplexed why the decl is there in the first place, but I assume you have a reason for it not conveyed in your question). – WhozCraig May 30 '16 at 00:02
  • suggest adding a tag according to your compiler, as the behaviour of `__declspec(dllimport)` is not covered by the C++ Standard – M.M May 30 '16 at 00:13
  • 1
    @WhozCraig Thank you for understanding exactly what I was trying to ask. You are 100% correct. Removing ONLY the declaration does indeed suppress the linker error. The declaration could be in error (it is from a third party library). My question is why does its presence (and its absence) result in different behavior. – ForeverLearning May 30 '16 at 00:46
  • @xaxxon Please read WhozCraig's comment. He has my issue down pat. – ForeverLearning May 30 '16 at 00:49
  • @xaxxon an MCVE is posted – M.M May 30 '16 at 05:19
  • @M.M "must include the desired behavior" – xaxxon May 30 '16 at 05:41
  • 1
    @xaxxon I think it's clear enough that the desired behaviour is for no linker error, and OP is asking why commenting out the marked line removes the linker error – M.M May 30 '16 at 05:43
  • @M.M why would there ever not be a linker error when the function has no definition? Or if it does have a definition, that is not provided and therefor there is no MCVE. I think the question is really "why does VS give weird linker errors in weird (possibly UB) situations".. which is a question of dubious value. – xaxxon May 30 '16 at 05:43
  • @xaxxon answering your first question is part of answering OP's question. The fact is that there is no linker error when the line is commented out and OP wants to know why. – M.M May 30 '16 at 06:02
  • The whole program behaviour is undefined according to the C++ Standard because of the use of `__declspec(dllimport)` so I don't think it is helpful to dismiss it as UB; the question is clearly in the context of some non-standard build setup – M.M May 30 '16 at 06:04
  • @M.M This question should really have been posed with "Why does visual studio fail to give an undefined reference error when __declspec(dllimport) is specified" -- then none of this waste of everyone's time would have had to happen and people could have immediately decided if spending time on trying to figure out yet another way VS is a bad compiler is worth the investment... so I just changed it. Let's see if it sticks. – xaxxon May 30 '16 at 06:51
  • 3
    Can't understand these people who see the words "undefined reference" in a post, ignore everything else in the post and mark it as "exact duplicate" of a giant thread that has basically nothing to do with the question. – M.M May 30 '16 at 07:01
  • 1
    @xaxxon A handful of commenters seem to exactly understand what I am asking. I suggest you please leave this thread alone and let whoever else want to "waste their time" make their own decisions about it. – ForeverLearning May 30 '16 at 13:47
  • Out of curiosity, did you remember to tell `cl` about whichever library `SomeFunc()` is defined in? You have to pass it the `.lib` file for the DLL you're importing the function from, too, so `link` can generate the proper `DllImport` code. The command line, whether generated automatically by the IDE or manually entered by you, should probably look something like `cl [options] MyTest.cpp SomeFunc.lib`. – Justin Time - Reinstate Monica May 30 '16 at 21:42
  • Apart from that, I think the issue is still there regardless of `TriggerIssue()`'s first declaration; it's just masked when inline because you don't actually call `TriggerIssue()`. Even if `TriggerIssue()` is inline, you'd probably have the same problem if you actually called it somewhere. The only difference appears to be that the first declaration forces it to compile it as a distinct function, thus calling `SomeFunc()`, instead of omitting it entirely since it's `inline` but wasn't referenced anywhere, and thus doesn't actually need to exist in the resulting program. Testing now. – Justin Time - Reinstate Monica May 30 '16 at 21:48
  • ...And tested. That's the issue, answering. – Justin Time - Reinstate Monica May 30 '16 at 21:53

2 Answers2

3

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().

Community
  • 1
  • 1
  • Kudos for the detailed analysis! BTW, I do understand that the link line will need a reference to the .lib that actually pulls in `SomeFunc` and keeps the linker happy. The whole objective of my post was to understand the difference in the behavior with and without the extraneous/erroneous *declaration* of `TriggerFunc` commented out. – ForeverLearning May 31 '16 at 00:01
  • Also, I do understand the references to ~Foo() in the object file (which in turn calls SomeFunc) is what led to the linker error. I just didn't realize that the first declaration effectively hid the inline definition and prevented the compiler from optimizing away the calls to Foo ctor and dtor. – ForeverLearning May 31 '16 at 00:08
  • @Dilip That appears to actually be part of the C++ standard, not specific to MSVC. According to [cppreference](http://en.cppreference.com/w/cpp/language/inline), "The inline specifier cannot re-declare a function that was already defined in the translation unit as non-inline." This would explain why the first declaration forces the compiler to generate the function, even if it isn't used. – Justin Time - Reinstate Monica May 31 '16 at 05:56
  • You're speaking in general terms but this only applies to VS (GCC for example happily compiles OP's (technically invalid) code), and due to what appears to be come kind of quirk, only to `extern "C"` functions (VS has no problems compiling the code if `extern "C"` is removed). The presence of `inline` and / or multiple declarations has no standard bearing on if and when a compiler will emit a function in the compiled object. – user657267 May 31 '16 at 06:00
  • @user657267 Interesting. I just did a couple quick tests (one removing `extern "C"`, and the other removing the `"C"` but leaving the `extern`), and it seems that the part that MSVC is specifically bothered by is the `extern` declaration, which appears to force it to generate a function that can be externally linked, indirectly causing the issue. If you remove the `extern "C"` from both declarations of `TriggerIssue()`, it happily optimises it out entirely, generating a symbol table identical to **Example 1**. Thanks for pointing that out. – Justin Time - Reinstate Monica May 31 '16 at 18:24
  • I just did a quick test (with TutorialsPoint's online GCC compiler, since I don't have GCC on this computer), and it appears that GCC is more willing to optimise inline functions declared `extern` out. If I compile his code (changing `__declspec(dllimport)` to `extern`, but making no other changes), then examine the symbol table with `objdump -t a.out`, there are no symbols for any of the problem functions (`TriggerIssue()` (`TriggerIssue`), `~Foo()` (`_ZN3FooD1Ev` or `_ZN3FooD2Ev`), or `SomeFunc()` (`_Z8SomeFuncv`)). If I add a call to `TriggerIssue()` to `main()`, GCC has the same problem. – Justin Time - Reinstate Monica May 31 '16 at 19:05
  • So, it seems that the reason GCC is more forgiving here is that MSVC is more cautious about optimising out a symbol it thinks you might want to export. – Justin Time - Reinstate Monica May 31 '16 at 19:07
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.

user657267
  • 20,568
  • 5
  • 58
  • 77
  • I suppose I cannot accept 2 answers? I will upvote though. Thanks for looking into this! – ForeverLearning May 31 '16 at 10:49
  • @Dilip Although the other answer gave a very good technical rundown of what's going on behind the scenes, the ultimate tl;dr is; a. You must define functions that are called, even if they are in unused code, and b. `inline` means "this function may be defined more that once in the program", nothing more. Your compiler has the final say on actual code inlining, and will do so independently of the `inline` specifier (assuming of course it can see the code of the definition). – user657267 May 31 '16 at 12:24