Tuesday, June 10, 2008

What! Are you blind?

I didn't say it out loud, but I was thinking something similar.
Personification of inanimate objects like compilers is daft,
but it feels good. That's my excuse anyway.

Here's the situation.

Given this C++/CLI code:

template< typename T >
ref struct A
{
~A()
{
}
};

ref struct B
{
A<int>^ a;
}

The linker generates this error:
error LNK2020: unresolved token (06000414) A<int>::Dispose

Knowing that in C++/CLI, the destructor syntax generates a Dispose function for ref classes/structs, the usual resolution is to ensure that A class has a destructor defined.

Which is where you say "What! Are you blind?". Because there it is. And no matter how many times you check it to make sure it exists and hit the recompile button, the linker still spits out the same error. I know. I tried a lot of times.

The solution? Change the member declaration in B to:
A<int> a;


The reason? There are conflicting behaviours between the C++ template compilation and the C++/CLI MSIL generator. By declaring a destructor, the MSIL generation insists that the class must have a Dispose method. But the C++ template compilation will not emit definitions of functions in template classes unless the function is actually used.

By using the handle syntax (^), but never manually calling a->Dispose(), the compiler will never emit the Dispose function. Removing the handle syntax, such that the "a" variable exists on the stack (at least notionally), means that behind the scenes the compiler generates the call to a->Dispose() when the variable goes out of scope.

In native C++, the template optimisation is exactly what most programmers want. Why have a function defined if it is never called?

However an MSIL class in an assembly must be fully defined, regardless of what functions are called.

Hence, unless you manually call Dispose, or use the stack based syntax, the compiler never generates the Dispose function for the linker to find, which is invalid for an MSIL class in an assembly.

Of course, the resulting error is useful because I realised that I hadn't managed the object properly. Unfortunately this can only be considered serendipitous, because should many objects of type A exist, but only one of them get Disposed properly (or even improperly), the unresolved external error won't show.

The unanswered question. It seems that there is something special about the Dispose function (or how it is generated with the destructor syntax), because the same is not true of an ordinary function. E.g.

template< typename T >
ref struct A
{
~A()
{
}

void f()
{
}
};

ref struct B
{
A<int> a;
}

This code does not generate an unresolved external error for the f function. Further investigation with ILDasm is required.

Note: This post is relevant to Visual Studio 2005. I am not sure of how VS 2008 behaves.

3 comments:

Rouge said...
This comment has been removed by the author.
Rouge said...

Hi,

thanks for the explanation. This is still valid in VS2008.

However instead of declaring it on the stack one can just define a destructor for the class which includes the object and delete it there, thus the compiler knows it has to build the decompose function.

Cheers,
Martin

Anonymous said...

Also still valid in 2012