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

19
  • 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). Commented May 30, 2016 at 0:02
  • 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. Commented May 30, 2016 at 0:46
  • 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 Commented May 30, 2016 at 5:43
  • 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. Commented May 30, 2016 at 7: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. Commented May 30, 2016 at 13:47

2 Answers 2

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

Sign up to request clarification or add additional context in comments.

7 Comments

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.
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.
@Dilip That appears to actually be part of the C++ standard, not specific to MSVC. According to cppreference, "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.
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 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.
|
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.

2 Comments

I suppose I cannot accept 2 answers? I will upvote though. Thanks for looking into this!
@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).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.