Introduction After my last article about usage of std::auto_ptr I want to stop on interesting thing which I have faced during one project. It has name incomplete type and you can challenge with it through std::auto_ptr one day. Of course you can face it in other cases, but after my advice to use auto pointer if your compiler doesn't support C++11, I should tell about incomplete type with std::auto_ptr. Nature of incomplete type or let's speak about forward declaration Probably you have already met such thing in C++, but if combination of words "incomplete type" doesn't say anything to you, it is because of work behind your eyes. Incomplete type can be in few cases and most often they are: * you create array without any size: extern char chrarr[]; // chrarr is incomplete type. * you declare class\structure\union\enum without its full definition:
// Resource is incomplete type.
class Resource;
class IncompleteClass
{
private:
Resource *field;
};
I'm going to talk about the later case. It is called forward declaration. It is very useful trick in C++ and can be used when in declaration of class it is enough to use forward type for pointers and references. Indeed forward declaration suffices as type for pointers and references because for compiler it's enough to know size of address (e.g. 4 bytes for 32-bit system and 8 bytes for 64-bit system). Full use case looks like this: * header file "IncompleteClass.h":
// Resource is incomplete type.
class Resource;
class IncompleteClass
{
public:
IncompleteClass();
~IncompleteClass();
private:
Resource *resource;
};
* source file "IncompleteClass.cpp":
#include "IncompleteClass.h"
#include "Resource.h"
IncompleteClass::IncompleteClass()
: resource(new Resource())
{
}
IncompleteClass::~IncompleteClass()
{
delete resource;
}
As you can see in header "IncompleteClass.h" file we have declared “Resource” type and it is incomplete. We don't have much information about it, but we successfully set this type for "resource" field. With incomplete “Resource” type in header file we can't: * construct “Resource” instance; * get sizeof(Resource); * use pointer arithmetic; * use “Resource” type by value; * inherit from “Resource”. Incomplete type has such restrictions, because compiler doesn't have much information. But at the same time in source file "IncompleteClass.cpp" we have included file "Resource.h" which has all information about this class. It gives us the possibility to work with “Resource” instance as usual. So why forward declaration is necessary? C++ programmers use them in several cases: * reduce dependencies between header files; * resolve circle referencing of header files; * hide implementation of internal classes of a library; * provide backward compatible binary interface for a shared library. Last two are very useful when you deliver your product in binary form with header files and don't want to show internal implementation or break binaries that are already compiled against your library. So at the moment everything is OK, but lets use std::auto_ptr for managing lifetime of “Resource” instance. Forward declaration + std::auto_ptr = UB or something what you don't expect With std::auto_ptr IncompleteClass will look like this: * header file "IncompleteTypeUB.h":
#include <memory>
// Forward declaration of Resource.
class Resource;
class IncompleteClass
{
public:
IncompleteClass();
private:
std::auto_ptr<Resource> resource;
};
* source file "IncompleteTypeUB.cpp":
#include "IncompleteTypeUB.h"
#include <Resource.h>
IncompleteClass::IncompleteClass()
: resource(new Resource())
{
}
“Resource” implementation you can see there. It just prints information to standard output on its creation and destruction. Let's run such code:
{
IncompleteClass incomplete;
} // UB: probably crash here or leak.
When I first stumbled upon such situation it caused segmentation fault, but during writing this article with another compiler I got in output: Resource is constructed. and program completed as usual, but it didn't say anything about destruction of “Resource” instance! It means I got leak in my program! “Why? We have used std::auto_ptr!” You would say. If you go back to header of IncompleteClass, you will see that it doesn't have destructor. I hope you know that if C++ compiler doesn't find destructor of class, it will be auto-generated. So in declaration of IncompleteClass there will be generated destructor which must call destructors of all fields of class. So it will call destructor of std::auto_ptr which must free “Resource” instance. But at that time “Resource” type is still incomplete. Compiler doesn't know about correct destructor of “Resource”! So it puts something that developer of compiler thinks about this world, because it is undefined behavior. Ways to deal with incomplete type issue or at the beginning of cross roads What can we do about this? 1. Simpler way – include header file of “Resource” class in header of “IncompleteClass” or where it is used. It will give necessary information for compiler to auto-generate correct destructor for “IncompleteClass”. See:
#include "IncompleteTypeUB.h"
#include “Resource.h”
void UseIncompleteClass()
{
IncompleteClass incomplete;
}
It prints: Resource is conscructed. Resource is released. But it isn't great because what if you use forward declaration for hiding internal implementation of Resource (one of the reasons to use forward declaration listed above). It corrupts our plans! 2. Another way if you use std::auto_ptr is to add destructor for “IncompleteClass” and write its definition in source file. Check header file now:
#include <memory>
class Resource;
class IncompleteWithDtor
{
public:
IncompleteWithDtor();
~IncompleteWithDtor();
private:
std::auto_ptr<Resource> resource;
};
And source file:
#include "IncompleteWithDtor.h"
#include "Resource.h"
IncompleteWithDtor::IncompleteWithDtor()
: resource(new OtherResource())
{
}
IncompleteWithDtor::~IncompleteWithDtor()
{
}
Destructor in source file is empty, but in this translation unit compiler has information about “Resource” class. It will help to call correct destructor of Resource instance. Weakness of such variant is that actually you don't want to declare destructor of “IncompleteClass”, but development forces you, because you used std::auto_ptr to avoid managing lifetime of an instance. It is up to your pride: if you choose this way or not. 3. Actually what I said is true for compilers before C++11. If you compile such code with C++11 and replace std::auto_ptr with std::shared_ptr it will work without needless destructor or direct header including. Look at the following header file:
#include <memory>
class Resource;
class IncompleteSharedPtr
{
public:
IncompleteSharedPtr();
private:
std::shared_ptr<Resource> resource;
};
and this source file:
#include "IncompleteSharedPtr.h"
#include <Resource.h>
IncompleteSharedPtr::IncompleteSharedPtr()
: resource(std::make_shared<Resource>())
{
}
Unfortunately, you can't use std::unique_ptr with incomplete type instead of std::auto_ptr because it tries to get size of() of held instance in static_assert() check. Anyway it causes compile error for my version of GCC and I don't think that it can be cross platform because of this. But likely std::shared_ptr can work with incomplete type without extra destruction for class-keeper or direct including of forwarded class. Conclusion. In this article I have told about combination of std::auto_ptr and incomplete type through forward declaration of class, because such love story has pitfalls. There is an issue with undefined behavior on destruction of held instance of std::auto_ptr. In some cases it can be crash of the application, in some cases it can be leak of held instance. I have shown three ways how to handle such situation: · direct including header of held class (worst); · define empty destructor (better); · use C++11 and std::shared_ptr (good). As usual you can find article's code in my Github repository, but at this time it has little difference with code in article because I have faced issues with compiling example code at one build. If you are looking for my previous articles, you can find them here: · “Do not muddle pointers and references”; · “Extended talk about references and pointers”; · “Haters gonna hate or life without pointers before C++11”; · “Haters gonna hate or life without pointers in C++11 times”.