Implementing a smart pointer which works with incomplete types

J

Juha Nieminen

I know this has been explained before, but I just can't find the post.
Also, I can't find any info on this in the C++ FAQ Lite.

In order to make a smart pointer class work with incomplete types,
some trickery is needed. I found one solution somewhere, which looks
like this:

template<typename T>
class GenericPtr
{
private:
typedef void(*DeleteFunctionType)(T* p);
DeleteFunctionType deleteFunction;
T* ptr;
static void doDelete(T* p) { delete p; }

public:
GenericPtr(T* p): ptr(p), deleteFunction(doDelete) {}
~GenericPtr() { deleteFunction(ptr); }

// other functions omitted
};

I just can't understand why this works. What's the difference between
executing 'delete' directly in the destructor and the destructor calling
a function, which executes the 'delete'? I would like to understand this
(because understanding the idea makes it easier to remember the technique).
 
P

Pete Becker

I know this has been explained before, but I just can't find the post.
Also, I can't find any info on this in the C++ FAQ Lite.

In order to make a smart pointer class work with incomplete types,
some trickery is needed. I found one solution somewhere, which looks
like this:

template<typename T>
class GenericPtr
{
private:
typedef void(*DeleteFunctionType)(T* p);
DeleteFunctionType deleteFunction;
T* ptr;
static void doDelete(T* p) { delete p; }

public:
GenericPtr(T* p): ptr(p), deleteFunction(doDelete) {}
~GenericPtr() { deleteFunction(ptr); }

// other functions omitted
};

I just can't understand why this works. What's the difference between
executing 'delete' directly in the destructor and the destructor calling
a function, which executes the 'delete'? I would like to understand this
(because understanding the idea makes it easier to remember the technique).

There's no difference. This code requires a complete type, because it
deletes a pointer to that type. What it's getting at, although it
doesn't actually implement it, is the possibility of having a deleter
function defined somewhere else, where the type is complete.

For a complete implementation of this approach, see tr1's shared_ptr
(also in boost).
 
P

Phil Endecott

Juha said:
I know this has been explained before, but I just can't find the post.
Also, I can't find any info on this in the C++ FAQ Lite.

In order to make a smart pointer class work with incomplete types,
some trickery is needed. I found one solution somewhere, which looks
like this:

template<typename T>
class GenericPtr
{
private:
typedef void(*DeleteFunctionType)(T* p);
DeleteFunctionType deleteFunction;
T* ptr;
static void doDelete(T* p) { delete p; }

public:
GenericPtr(T* p): ptr(p), deleteFunction(doDelete) {}
~GenericPtr() { deleteFunction(ptr); }

// other functions omitted
};

I just can't understand why this works. What's the difference between
executing 'delete' directly in the destructor and the destructor calling
a function, which executes the 'delete'? I would like to understand this
(because understanding the idea makes it easier to remember the technique).

This came up on the Boost list a while ago:
http://thread.gmane.org/gmane.comp.lib.boost.devel/165820/focus=166211

That points to this page:
http://www.octopull.demon.co.uk/arglib/TheGrin.html

which I seem to recall had an excellent description of the issues - but
it is now inaccessible! You may like to search for it in the Internet
Archive as it could be the "can't find the post" that you were thinking of.

My recollection is that the crux is that the type can be incomplete at
the time when the pointer type template is instantiated, but it must be
complete at the time when the delete function template is instantiated.


Phil.
 
J

Juha Nieminen

Pete said:
There's no difference.

Well, my tests with gcc disagree with this. That type of smart pointer
class correctly calls the destructor of the object, while a more
"traditional" doesn't.

Getting the situation to happen is a bit complicated, but I have tried
to simplify it as much as possible:

//==================== GenericPtr.hh ====================
template<typename T>
class GenericPtr1 // "Traditional" smart pointer
{
T* ptr;

public:
GenericPtr1(T* p): ptr(p) {}
~GenericPtr1() { delete ptr; }
};

template<typename T>
class GenericPtr2 // A version which should work with incomplete types
{
T* ptr;
typedef void(*DeleteFunctionType)(T* p);
DeleteFunctionType deleteFunction;
static void doDelete(T* p) { delete p; }

public:
GenericPtr2(T* p): ptr(p), deleteFunction(doDelete) {}
~GenericPtr2() { deleteFunction(ptr); }
};


//==================== AClass.hh ====================
#include "GenericPtr.hh"

class ClassToBeManaged;

class AClass
{
GenericPtr1<ClassToBeManaged> ptr1;
GenericPtr2<ClassToBeManaged> ptr2;

public:
AClass();
};


//==================== AClass.cc ====================
#include "AClass.hh"
#include "ClassToBeManaged.hh"

AClass::AClass():
ptr1(new ClassToBeManaged(1)), ptr2(new ClassToBeManaged(2))
{}


//==================== ClassToBeManaged.hh ====================
class ClassToBeManaged
{
int id;

public:
ClassToBeManaged(int i);
~ClassToBeManaged();
};


//==================== ClassToBeManaged.cc ====================
#include "ClassToBeManaged.hh"
#include <iostream>

ClassToBeManaged::ClassToBeManaged(int i): id(i)
{ std::cout << "Constructed " << id << std::endl; }

ClassToBeManaged::~ClassToBeManaged()
{ std::cout << "Destructed " << id << std::endl; }


//==================== test.cc ====================
#include "AClass.hh"

int main()
{
AClass a;
}


Running this program produces:

Constructed 1
Constructed 2
Destructed 2

This means that GenericPtr1 did not call the destructor of
ClassToBeManaged properly, while GenericPtr2 did.
 
J

James Kanze

Well, my tests with gcc disagree with this. That type of smart pointer
class correctly calls the destructor of the object, while a more
"traditional" doesn't.

I think you're quoting Pete out of context. He went on to
clearly explain what he meant: there must be a complete
definition of the class. The question is where:

-- for std::auto_ptr, at least according to the standard (in
practice, most implementations are a bit more tolerant), any
time the class template is instantiated,

-- for a "typical" smart pointer, where ever the destructor of
the smart pointer is instantiated, and

-- and for boost::shared_ptr, where the initial constructor of
the smart pointer is instantiated.

There's "no difference" in the sense that the smart pointer must
see the complete definition at some point. There are
differences with regards to where.
 
J

James Kanze

which I seem to recall had an excellent description of the
issues - but it is now inaccessible! You may like to search
for it in the Internet Archive as it could be the "can't find
the post" that you were thinking of.
My recollection is that the crux is that the type can be
incomplete at the time when the pointer type template is
instantiated, but it must be complete at the time when the
delete function template is instantiated.

Or in the case of boost::shared_ptr, which uses an object
constructed at the time the pointer is constructed to do the
delete, at the time the constructor is instantiated. (Roughly
speaking, you must pass the constructor a pointer on which it
can call delete without problems---the template argument
deduction mechanism takes care of the rest. And to call delete
without problems, the class definition must be available.)
 
J

Juha Nieminen

Juha said:
template<typename T>
class GenericPtr
{
private:
typedef void(*DeleteFunctionType)(T* p);
DeleteFunctionType deleteFunction;
T* ptr;
static void doDelete(T* p) { delete p; }

public:
GenericPtr(T* p): ptr(p), deleteFunction(doDelete) {}
~GenericPtr() { deleteFunction(ptr); }

// other functions omitted
};

Since nobody seems to be able to give a *clear* answer to the
question, let's see if I have figure this out correctly:

Template functions seem to have this funny property that they are not
"instantiated" (ie. an actual physical function is not created) until
they are referred to, in one way or another. This seems to be so even if
the function in question is inside a template class which *is* being
instantiated.
For example, even though GenericPtr is being referred to like:

class SomeClass;
class SomeOtherClass
{
GenericPtr<SomeClass> ptr;

public:
SomeOtherClass();
};

the 'doDelete()' function inside it is *not* being instantiated because
nothing is referring to it yet. There may be all kinds of code inside
that function which assumes some content in the incomplete type, like
for example:

static void doDelete(T* p) { p->printMsg("Hello"); delete p; }

yet this will not cause any error, even though at this point T refers to
an incomplete type, because 'doDelete()' is not being instantiated yet.

The destructor of 'GenericPtr' seems to have a special role, though.
If 'SomeOtherClass' has no destructor, then the destructor of
'GenericPtr' is being instantiated immediately. Thus this destructor
cannot refer to the incomplete type in any way. The destructor in
question is only referring to the function pointer, which is being
instantiated immediately, but that's not a problem because it's just a
pointer and has no code in itself.

If 'SomeOtherClass' *does* have a destructor, however, then the
instantiation of the destructor of 'GenericPtr' seems to be delayed,
transferred to the instantiation of the destructor of 'SomeOtherClass'.
Thus in this case the destructor of 'GenericPtr' could refer to anything
inside T without problems.

I haven't actually tested, but I would guess that the constructor has
similar behavior: Since 'SomeOtherClass' has a constructor, the
constructor instantiation of 'GenericPtr' is being transferred to the
constructor implementation of 'SomeOtherClass' instead of being
instantiated immediately.

When the constructor of 'SomeOtherClass' is implemented, that
instantiates the constructor of 'GenericPtr', which in turn instantiates
'doDelete()' because the constructor refers to it. If in this context
'SomeClass' has been fully declared, no problems happen. A pointer to
this 'doDelete()' instance is stored, and everything works fine.

Does this "delayed instantiation" of functions work also with regular
member functions, or does it only work for static class functions?
 
B

Bo Persson

Juha said:
Since nobody seems to be able to give a *clear* answer to the
question, let's see if I have figure this out correctly:

Template functions seem to have this funny property that they are
not "instantiated" (ie. an actual physical function is not created)
until they are referred to, in one way or another. This seems to be
so even if the function in question is inside a template class
which *is* being instantiated.
For example, even though GenericPtr is being referred to like:

class SomeClass;
class SomeOtherClass
{
GenericPtr<SomeClass> ptr;

public:
SomeOtherClass();
};

the 'doDelete()' function inside it is *not* being instantiated
because nothing is referring to it yet. There may be all kinds of
code inside that function which assumes some content in the
incomplete type, like for example:

static void doDelete(T* p) { p->printMsg("Hello"); delete p; }

yet this will not cause any error, even though at this point T
refers to an incomplete type, because 'doDelete()' is not being
instantiated yet.

It is true that member functions are only instantiated if they are
used, with a C++ specific definition of "use".

However, in this case it doesn't apply becase doDelete is "used" in
GenericPtr's constructor, when its address is taken. At that point, T
must be a complete type.

[incorrect reasoning snipped]
When the constructor of 'SomeOtherClass' is implemented, that
instantiates the constructor of 'GenericPtr', which in turn
instantiates 'doDelete()' because the constructor refers to it. If
in this context 'SomeClass' has been fully declared, no problems
happen. A pointer to this 'doDelete()' instance is stored, and
everything works fine.

In another subthread, Pete Becker hinted that doDelete() should be
defined somewhere else where the type to be deleted is complete. I
belive that this is exactly what happens in your test code. Therefore
it works, sometimes.

A real pointer class would not have a doDelete() member, but could
take a pointer to a deleter as a parameter to the constructor. That
way, it could refer to a function defined in a place where the pointed
to type is complete.


Bo Persson
 
J

Juha Nieminen

Bo said:
It is true that member functions are only instantiated if they are
used, with a C++ specific definition of "use".

That's what I said.
However, in this case it doesn't apply becase doDelete is "used" in
GenericPtr's constructor, when its address is taken. At that point, T
must be a complete type.

And the constructor of GenericPtr is instantiated when the constructor
of 'SomeOtherClass' is implemented (which in turn means that
'doDelete()' is also instantiated at that place), not earlier. Is this
incorrect?
[incorrect reasoning snipped]

Instead of saying it's incorrect, could you please tell me what was it
that was incorrect and what is the correct explanation?
In another subthread, Pete Becker hinted that doDelete() should be
defined somewhere else where the type to be deleted is complete. I
belive that this is exactly what happens in your test code. Therefore
it works, sometimes.

Sometimes? When does it not work?

The only situation which I can think of where it may not work is when
'SomeOtherClass' has no constructor (or, more precisely, it only has the
default compiler-generated constructor). AFAIK that's the known
limitation of the technique: The constructor of the class which has that
type of smart pointers as members must be explicitly implemented.

But seemingly I have missed something?
 
B

Bo Persson

Juha said:
That's what I said.


And the constructor of GenericPtr is instantiated when the
constructor of 'SomeOtherClass' is implemented (which in turn means
that 'doDelete()' is also instantiated at that place), not earlier.
Is this incorrect?

In this example it is correct. In a larger program, instantiations can
happen in several places (later to be fixed up by the linker). If in
one of those places the type happens to be incomplete, the entire
scheme fails.
[incorrect reasoning snipped]

Instead of saying it's incorrect, could you please tell me what
was it that was incorrect and what is the correct explanation?

The reasoning about user defined or default constructors is just not
valid. It doesn't have that effect.

The example code works just because the template is instantiated only
once, and in a place where the appropriate header files are included
first.
Sometimes? When does it not work?

When the instantiations do not happen in places where all the types
are complete.
The only situation which I can think of where it may not work is
when 'SomeOtherClass' has no constructor (or, more precisely, it
only has the default compiler-generated constructor). AFAIK that's
the known limitation of the technique: The constructor of the class
which has that type of smart pointers as members must be explicitly
implemented.

No, it has nothing to do with that, except that with a user defined
constructor and a non-templated class, you can control where the
instantiations happen.



Bo Persson
 
J

Juha Nieminen

Bo said:
[incorrect reasoning snipped]
Instead of saying it's incorrect, could you please tell me what
was it that was incorrect and what is the correct explanation?

The reasoning about user defined or default constructors is just not
valid. It doesn't have that effect.

The example code works just because the template is instantiated only
once, and in a place where the appropriate header files are included
first.

I know that the example code lacked a proper copy constructor and
assignment operator, for brevity, and that those need to be carefully
designed as well in order for them to work properly with incomplete
types (iow. in the same way as the destructor cannot refer to 'doDelete'
directly, neither can the copy constructor nor the assignment operator).

However, other than that I can't think of any other situation where
they could cause a problem. Naturally if the incomplete type is still
incomplete when the regular constructor of the smart pointer is called,
that will cause an error, but that's a defined limitation of this
technique, and thus to be expected.

If you could give me an example of a situation where problems happen,
I would appreciate it. I would like to understand this technique fully.
When the instantiations do not happen in places where all the types
are complete.

This technique requires that the type be complete when the smart
pointer is constructed. It's a known limitation. I don't really see it
as a problem (at least not for the things I usually use smart pointers for).
 
J

James Kanze

Bo Persson wrote:

[...]
Sometimes? When does it not work?

In a situation where the class is not fully defined when
doDelete is instantiated?

I'm not sure of the exact context, but I think what you're
suggesting is the standard solution used to ensure that you can
instantiate a smart pointer class template over an incomplete
type. Although it may be more than is needed.
The only situation which I can think of where it may not work
is when 'SomeOtherClass' has no constructor (or, more
precisely, it only has the default compiler-generated
constructor). AFAIK that's the known limitation of the
technique: The constructor of the class which has that type of
smart pointers as members must be explicitly implemented.

Why is that limitation present? If the class is defined (i.e.
the class is not an incomplete type), then the compiler knows
what to do in case of destruction. Regardless of whether the
constructor is user defined or not.
 
J

Juha Nieminen

James said:
In a situation where the class is not fully defined when
doDelete is instantiated?

Since doDelete() is referred to only in the constructor of the smart
pointer (referring to it directly can be avoided in the copy constructor
and the assignment operator, and naturally in the destructor), the only
situation where the class must be fully defined is when the smart
pointer is constructed.
As I have commented in the other posts, this is a requirement for the
technique.
I just want to make sure I have understood this fully.

Any modern C++ compiler will give big warnings about not being able to
access the destructor of the object if the smart pointer is constructed
in a context where the class in question is an incomplete type. (With an
intrusive smart pointer it's even better because the doDelete() actually
accesses the object in question (to decrement the reference count),
which will outright cause a compiler error if attempted to do so with an
incomplete type.)

So with a decent compiler it's not so much a question of "I'm not sure
if the destructor of this object will be called or not" (because the
decent compiler should warn you if it isn't), but more about "how can I
make it work with an incomplete type?"
Why is that limitation present? If the class is defined (i.e.
the class is not an incomplete type), then the compiler knows
what to do in case of destruction. Regardless of whether the
constructor is user defined or not.

I'm not exactly sure what you mean, but thinking about it, there might
not be any problem after all. Even if the class has no constructor, that
is, the situation is like this:

class ClassToBeManaged;
class AClass
{
SmartPointer<ClassToBeManaged> ptr;

public:
// No constructor nor destructor defined here.

// A function which allocates an instance of ClassToBeManaged and
// gives it to 'ptr':
void foo();
};

Then as long as 'ClassToBeManaged' is fully declared when 'foo()' is
implemented, there should be no problem (as long as the assignment
operator of 'SmartPointer' is properly designed).

In other words:

// AClass.cc
#include "ClassToBeManaged.hh" // Full declaration

void AClass::foo()
{
// ClassToBeManaged is fully declared in this context and thus
// the construction below works. As long as the assignment operator
// of 'SmartPointer' is properly designed, there should be no
// problem with that either:
ptr = new ClassToBeManaged;
}
 
J

James Kanze

James Kanze wrote:

[...]
I'm not exactly sure what you mean, but thinking about it, there might
not be any problem after all. Even if the class has no constructor,

Every class type has a constructor. Period.
that is, the situation is like this:
class ClassToBeManaged;
class AClass
{
SmartPointer<ClassToBeManaged> ptr;
public:
// No constructor nor destructor defined here.
// A function which allocates an instance of ClassToBeManaged and
// gives it to 'ptr':
void foo();
};
Then as long as 'ClassToBeManaged' is fully declared when 'foo()' is
implemented, there should be no problem (as long as the assignment
operator of 'SmartPointer' is properly designed).

Correct. The only time you might have a problem is in a case
like:

class ClassToBeManaged ;
ClassToBeManaged* factory() ;

// ...
SmartPointer< ClassToBeManaged > ptr( factory() ) ;

And as you say, good compilers will warn here.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,011
Latest member
AjaUqq1950

Latest Threads

Top