Why does this virtual destructor trigger an unresolved external?
Asked Answered
F

7

19

Consider the following:

In X.h:

class X
{
    X();
    virtual ~X();
};

X.cpp:

#include "X.h"

X::X()
{}

Try to build this (I'm using a .dll target to avoid an error on the missing main, and I'm using Visual Studio 2010):

Error 1 error LNK2001: unresolved external symbol "private: virtual __thiscall X::~X(void)" (??1X@@EAE@XZ)

Small modifications result in a successful build, however:

X.h:

class X
{
    inline X(); // Now inlined, and everything builds
    virtual ~X();
};

or

X.h:

class X
{
    X();
    ~X(); // No longer virtual, and everything builds
};

What causes the unresolved external in the linker when the .dtor is virtual or when the .ctor isn't inlined?

EDIT:

Or, perhaps more interestingly, why do I not get an unresolved external if I make the destructor non-virtual, or if I inline the constructor?

Fainthearted answered 24/8, 2010 at 20:34 Comment(5)
I'm sure I understand the question/variants, but the bottom line is: if it's going to be called, it needs a definition. In your first variant, you never define the destructor, which is exactly what the error says. I don't see how the variations build, since there's still no definition of the constructor or destructor I can see.Ninefold
Yet they do. Thus my confusion. It's a pretty simple code sample, and none of my coworkers has a definitive answer for this behavior. The best guess was that something's undefined and we end up in vendor-specific territory, but then I'm curious about what precisely is undefined that would trigger the differing behaviors.Fainthearted
Imposable to tell unless you show us everything you are building!Shenyang
@Martin York: That is everything I'm building! Literally! Ok, I may have omitted a carriage return in front of one of the files, but seriously, that is everything in the project. You could copy this into your own scratch library in like 20 seconds. It's this dead simple.Fainthearted
In the first one it needs the address of the destructor to build the virtual table. In the second one: Since the constructor is never actually built it never needs to build the virtual table and thus does not need the address of the destructor. In the third one we dont need the address of the destructor when building the constructor (as it is no longer in the virtual table);.Shenyang
S
24

Situation 1:

You have the code for the constructor.
So it builds the constructor into the object file. The constructor needs the address of the destructor to put into the virtual table because it can not find it the constructor can not be built.

Situation 2: (inline constructor)

The compiler decides it does not need to build the constructor (as it will be inlined).
As such it does not plant any code and therefore does not need the address of the destructor.

If you instanciate an object of type X it will again complain.

Situation 3: (non virtual destructor)

You don't need the address of the destructor to build the constructor.
So it does not complain.

It will complain if you instantiate an object of type X.

Shenyang answered 24/8, 2010 at 20:54 Comment(0)
O
10

You need to give a body to the virtual destructor:


class X
{
    X();
    virtual ~X() {}
};
Oquassa answered 24/8, 2010 at 20:37 Comment(1)
Thank you for the suggestion, but this isn't a plz-send-teh-codez how-do-i-get-rid-of-the-error question. Rather it's a question about understanding the tool that we all make a living with a bit more closely.Fainthearted
M
5

In C++ functions have to be defined if and only if they are used in your program (see the ODR in 3.2/2). In general, non-virtual functions are used if they are called from potentially evaluated expressions. Any non-pure virtual function is considered unconditionally used. When [non-virtual] special member functions are used is defined in dedicated locations of the language standard. And so on.

  • In your first example, you declared your destructor as a non-pure virtual function. This immediately means that your destructor is used in your program. This, in turn, means that the definition of that destructor is required. You failed to provide a definition, so the compiler reported an error.

  • In your third example the destructor is non-virtual. Since you are not using the destructor in your program, no definition is required and the code compiles (see 12.4 for detailed description of what constitutes a use of a destructor).

  • In your second example you are dealing with a quirk of the implementation, triggered by the fact that the constructor is inlined. Since the destructor is non-pure virtual, the definition is required. However, your compiler failed to detect the error, which is why the code seems to compile successfully. You can dig for the reasons of this behavior in the details of the implementation, but from the C++ point of view this example is as broken as the first one for exactly the same reason.

Mako answered 24/8, 2010 at 21:6 Comment(0)
G
2

The answer to your first question,

What causes the unresolved external in the linker when the .dtor is virtual or when the .ctor isn't inlined?

...is, quite simply, that you don't have a definition for the destructor.

Now your second question is somewhat more interesting:

why do I not get an unresolved external if I make the destructor non-virtual, or if I inline the constructor?

And the reason is because your compiler didn't need X's destructor since you never instantiated X, so it threw your whole class away. If you try to compile this program, you will get an unresolved external:

class X
{
public:
    X();
     ~X();
};

X::X() {};

int main()
{
    X x;
    return 0;
}

But if you comment out X x; it will compile just fine, as you have observed.

Now let's come back around to why it won't compile if the destructor if virtual. I'm speculating here, but I believe the reason is because, since you have a virtual destructor, X is now a polymorphic class. In order to lay-out polymorphic classes in memory, compilers that implement polymorphism using a vtable need the adresses to every virtual function. You haven't implemented X::~X, so an unresolved external results.

Why doesn't the compiler just throw X away as it did when X was not a polymorphic class? More speculation here. But I expect the reason is because even if you haven't directly instantiated X, it can't be sure that nowhere in your code does an X live, masqerading as something else. For an example, consider an abstract base class. In this case, you'll never instantiate Base directly and the code for Derived might be in a totally seperate translation unit. So when the compiler gets to this polymorphic class, it can't discard it even if it doesn't know you instantiated it.

Granadilla answered 24/8, 2010 at 21:4 Comment(0)
C
1

These aren't a complete program yet (or even a complete DLL). When you are getting the error, you are actually being helped, because X is unusable without a definition for ~X()

All it means is that this specific compiler instance needed a definition for it in some cases. Even if it compiles, it doesn't do anything.

Chaille answered 24/8, 2010 at 20:38 Comment(2)
This is a distilled sample asking about a particular behavior that was observed. I'm not interested in making it a complete program, I'm interested in understanding why it builds and doesn't build in the observed situations.Fainthearted
It doesn't build because of compiler-specific peculiarities. It probably should have because you never destroy an X, and it also could have just pruned all of the code out since it's never called. But, it didn't. The class as written isn't meaningful, so it's ok for it not to compile.Chaille
A
1

I have a suspicion on this one that this is implementation defined behavior. Here's why

$10.3/8- "A virtual function declared in a class shall be defined, or declared pure (10.4) in that class, or both; but no diagnostic is required (3.2)."

GCC gives error such as below, which again, is highly suggestive (to me at least) about a non-standard implementation detail of implementing virtual functions

/home/OyXDcE/ccS7g3Vl.o: In function X::X()': prog.cpp:(.text+0x6): undefined reference tovtable for X' /home/OyXDcE/ccS7g3Vl.o: In function X::X()': prog.cpp:(.text+0x16): undefined reference tovtable for X' collect2: ld returned 1 exit status

I am confused if a diagnostic is really required from a compiler for the OP code, so thought of posting this, even as I risk downvotes :). Of course, a good compiler should I guess.

Annulus answered 25/8, 2010 at 2:41 Comment(0)
A
0

You may be getting away with this because both constr and destr are private - if there is no other ref to class X in your build then the compiler may be deducing that the destr is not required, so lack of a definition is no biggie.

This does not explain to me why case 1 fails while 2 and 3 build OK though. Wonder what happens if both are made public?

Appraise answered 24/8, 2010 at 20:53 Comment(1)
I see Martin York has explained this perfectly above.Appraise

© 2022 - 2024 — McMap. All rights reserved.