ABC interfaces, smart pointers and factory functions

S

Steven T. Hatton

I have a couple questions about the design pattern presented in the example
quoted below. I can appreciate why the destructor is protected, but why is
it not virtual? I am forced to assume that I am not expected to derive
from X. But why impose such a restriction?

Also, what are the tradeoffs between declaring createX() to be namespace
local vs. being a static member function of X?

<quote url="http://www.boost.org/libs/smart_ptr/sp_techniques.html">
Using abstract classes for implementation hiding
Another widely used C++ idiom for separating inteface and implementation is
to use abstract base classes and factory functions. The abstract classes
are sometimes called "interfaces" and the pattern is known
as "interface-based programming". Again, shared_ptr can be used as the
return type of the factory functions:
// X.hpp:

class X
{
public:

virtual void f() = 0;
virtual void g() = 0;

protected:

~X() {}
};

shared_ptr<X> createX();

-- X.cpp:

class X_impl: public X
{
private:

X_impl(X_impl const &);
X_impl & operator=(X_impl const &);

public:

virtual void f()
{
// ...
}

virtual void g()
{
// ...
}
};

shared_ptr<X> createX()
{
shared_ptr<X> px(new X_impl);
return px;
}

A key property of shared_ptr is that the allocation, construction,
deallocation, and destruction details are captured at the point of
construction, inside the factory function. Note the protected and
nonvirtual destructor in the example above. The client code cannot, and
does not need to, delete a pointer to X; the shared_ptr<X> instance
returned from createX will correctly call ~X_impl.
</quote>
 
S

Salt_Peter

Steven said:
I have a couple questions about the design pattern presented in the example
quoted below. I can appreciate why the destructor is protected, but why is
it not virtual? I am forced to assume that I am not expected to derive
from X. But why impose such a restriction?

Boost's shared pointer will correctly invoke the derived ctor for you.
No virtual d~tor required.
If you need to know how it does that, read boost/checked_delete.hpp.

As far as create() is concerned, that depends on your needs. Consider
that a class might be a Factory with a create() static, public
function, have X_impl with private ctor(s) only and make the Factory a
friend of X_impl. It depends on the requirements.
Also, what are the tradeoffs between declaring createX() to be namespace
local vs. being a static member function of X?

<quote url="http://www.boost.org/libs/smart_ptr/sp_techniques.html">
Using abstract classes for implementation hiding
Another widely used C++ idiom for separating inteface and implementation is
to use abstract base classes and factory functions. The abstract classes
are sometimes called "interfaces" and the pattern is known
as "interface-based programming". Again, shared_ptr can be used as the
return type of the factory functions:
// X.hpp:

class X
{
public:

virtual void f() = 0;
virtual void g() = 0;

protected:

~X() {}
};

shared_ptr<X> createX();

-- X.cpp:

class X_impl: public X
{
private:

X_impl(X_impl const &);
X_impl & operator=(X_impl const &);

public:

virtual void f()
{
// ...
}

virtual void g()
{
// ...
}
};

shared_ptr<X> createX()
{
shared_ptr<X> px(new X_impl);
return px;
}

A key property of shared_ptr is that the allocation, construction,
deallocation, and destruction details are captured at the point of
construction, inside the factory function. Note the protected and
nonvirtual destructor in the example above. The client code cannot, and
does not need to, delete a pointer to X; the shared_ptr<X> instance
returned from createX will correctly call ~X_impl.
</quote>

#include <iostream>
#include <boost/shared_ptr.hpp>

class X
{
public:
X() { std::cout << "X()\n"; }
~X() { std::cout << "~X()\n"; }
};

class X_impl : public X
{
public:
X_impl() { std::cout << "X_impl()\n"; }
~X_impl() { std::cout << "~X_impl()\n"; }
};

int main()
{
boost::shared_ptr< X > sp_x(new X_impl);
}

/*
X()
X_impl()
~X_impl()
~X()
*/
 
K

Kai-Uwe Bux

Steven said:
I have a couple questions about the design pattern presented in the
example
quoted below. I can appreciate why the destructor is protected, but why
is
it not virtual? I am forced to assume that I am not expected to derive
from X. But why impose such a restriction?

Where did you get the idea that you are not supposed to derive from X? The
whole point of the snippet is to demonstrate that you can derive from X and
use a shared_ptr<X> as the return type of a factory. The class X_impl below
derives from X.

What may surprise you is this: shared_ptr<X> using the default deleter will
call the correct destructor (~X_impl in the example) regardless of whether
~X it was declared virtual.

Also, what are the tradeoffs between declaring createX() to be namespace
local vs. being a static member function of X?
Huh?

<quote url="http://www.boost.org/libs/smart_ptr/sp_techniques.html">
Using abstract classes for implementation hiding
Another widely used C++ idiom for separating inteface and implementation
is to use abstract base classes and factory functions. The abstract
classes are sometimes called "interfaces" and the pattern is known
as "interface-based programming". Again, shared_ptr can be used as the
return type of the factory functions:
// X.hpp:

class X
{
public:

virtual void f() = 0;
virtual void g() = 0;

protected:

~X() {}
};

shared_ptr<X> createX();

-- X.cpp:

class X_impl: public X
{
private:

X_impl(X_impl const &);
X_impl & operator=(X_impl const &);

public:

virtual void f()
{
// ...
}

virtual void g()
{
// ...
}
};

shared_ptr<X> createX()
{
shared_ptr<X> px(new X_impl);
return px;
}

A key property of shared_ptr is that the allocation, construction,
deallocation, and destruction details are captured at the point of
construction, inside the factory function. Note the protected and
nonvirtual destructor in the example above. The client code cannot, and
does not need to, delete a pointer to X; the shared_ptr<X> instance
returned from createX will correctly call ~X_impl.
</quote>


Best

Kai-Uwe Bux
 
S

Steven T. Hatton

Kai-Uwe Bux said:
Where did you get the idea that you are not supposed to derive from X? The
whole point of the snippet is to demonstrate that you can derive from X
and use a shared_ptr<X> as the return type of a factory. The class X_impl
below derives from X.

The problem arrises when there are multiple levels of derivation, and one of
the intermediate levels has something that needs to be destroyed. If the
top base class destructor isn't virtual, none of the derived destructors
will be called if/when its invoked directly.

#include <iostream>
struct VBase{
virtual ~VBase(){ std::cerr << "VBase::~VBase" << std::endl; }
};

struct VDerived: public VBase {
virtual ~VDerived(){ std::cerr << "VDerived::~VDerived" << std::endl; }
};

struct V: public VDerived{
virtual ~V(){ std::cerr << "V::~V" << std::endl; }
};

struct Base{
~Base(){ std::cerr << "Base::~Base" << std::endl; }
};

struct Derived: public Base {
~Derived(){ std::cerr << "Derived::~Derived" << std::endl; }
};

struct B: public Derived{
~B(){ std::cerr << "B::~B" << std::endl; }
};


int main() {
V* v_ptr(new V);
B* b_ptr(new B);
VBase* vbase_ptr(new V);
Base* base_ptr(new B);

std::cerr << "--------delete v_ptr--------" << std::endl;
delete v_ptr;
std::cerr << "--------delete b_ptr--------" << std::endl;
delete b_ptr;
std::cerr << "--------delete vbase_ptr--------" << std::endl;
delete vbase_ptr;
std::cerr << "--------delete base_ptr--------" << std::endl;
delete base_ptr;
}

--------delete v_ptr--------
V::~V
VDerived::~VDerived
VBase::~VBase
--------delete b_ptr--------
B::~B
Derived::~Derived
Base::~Base
--------delete vbase_ptr--------
V::~V
VDerived::~VDerived
VBase::~VBase
--------delete base_ptr--------
Base::~Base
What may surprise you is this: shared_ptr<X> using the default deleter
will call the correct destructor (~X_impl in the example) regardless of
whether ~X it was declared virtual.



Huh?

It's not uncommon in Java to have static member factory methods. It does
have the advantage of being able to access private members of the
constructed object without declaring a friend.
 
S

Steven T. Hatton

Salt_Peter said:
Boost's shared pointer will correctly invoke the derived ctor for you.
No virtual d~tor required.
If you need to know how it does that, read boost/checked_delete.hpp.

So what are the consequences of making it virtual? Other than getting GCC
to stop complaining, that is.
#include <iostream>
#include <boost/shared_ptr.hpp>

class X
{
public:
X() { std::cout << "X()\n"; }
~X() { std::cout << "~X()\n"; }
};

class X_impl : public X
{
public:
X_impl() { std::cout << "X_impl()\n"; }
~X_impl() { std::cout << "~X_impl()\n"; }
};

int main()
{
boost::shared_ptr< X > sp_x(new X_impl);
}

/*
X()
X_impl()
~X_impl()
~X()
*/
Hmmm....
 
K

Kai-Uwe Bux

Steven said:
The problem arrises when there are multiple levels of derivation, and one
of
the intermediate levels has something that needs to be destroyed. If the
top base class destructor isn't virtual, none of the derived destructors
will be called if/when its invoked directly.

#include <iostream>
struct VBase{
virtual ~VBase(){ std::cerr << "VBase::~VBase" << std::endl; }
};

struct VDerived: public VBase {
virtual ~VDerived(){ std::cerr << "VDerived::~VDerived" << std::endl; }
};

struct V: public VDerived{
virtual ~V(){ std::cerr << "V::~V" << std::endl; }
};

struct Base{
~Base(){ std::cerr << "Base::~Base" << std::endl; }
};

struct Derived: public Base {
~Derived(){ std::cerr << "Derived::~Derived" << std::endl; }
};

struct B: public Derived{
~B(){ std::cerr << "B::~B" << std::endl; }
};


int main() {
V* v_ptr(new V);
B* b_ptr(new B);
VBase* vbase_ptr(new V);
Base* base_ptr(new B);

std::cerr << "--------delete v_ptr--------" << std::endl;
delete v_ptr;
std::cerr << "--------delete b_ptr--------" << std::endl;
delete b_ptr;
std::cerr << "--------delete vbase_ptr--------" << std::endl;
delete vbase_ptr;
std::cerr << "--------delete base_ptr--------" << std::endl;
delete base_ptr;
}

--------delete v_ptr--------
V::~V
VDerived::~VDerived
VBase::~VBase
--------delete b_ptr--------
B::~B
Derived::~Derived
Base::~Base
--------delete vbase_ptr--------
V::~V
VDerived::~VDerived
VBase::~VBase
--------delete base_ptr--------
Base::~Base

Apparently, you missed the point entirely: this is all about *shared_ptr<>*.
Your example uses *raw pointers*. There is a difference. What you learned
about pointers does not apply one-to-one to shared_ptr<>. The point of the
snippet is to show that with shared_ptr the destructor does not need to be
virtual. Try to do the above code with shared_ptr:

#include <iostream>
#include <tr1/memory>

struct VBase{
virtual ~VBase(){ std::cerr << "VBase::~VBase" << std::endl; }
};

struct VDerived: public VBase {
virtual ~VDerived(){ std::cerr << "VDerived::~VDerived" << std::endl; }
};

struct V: public VDerived{
virtual ~V(){ std::cerr << "V::~V" << std::endl; }
};

struct Base{
~Base(){ std::cerr << "Base::~Base" << std::endl; }
};

struct Derived: public Base {
~Derived(){ std::cerr << "Derived::~Derived" << std::endl; }
};

struct B: public Derived{
~B(){ std::cerr << "B::~B" << std::endl; }
};


template < typename T >
void delete_shared ( std::tr1::shared_ptr<T> & s ) {
s = std::tr1::shared_ptr<T>();
}

int main() {
std::tr1::shared_ptr<V> v_ptr(new V);
std::tr1::shared_ptr<B> b_ptr(new B);
std::tr1::shared_ptr<VBase> vbase_ptr(new V);
std::tr1::shared_ptr<Base> base_ptr(new B);

std::cerr << "--------delete v_ptr--------" << std::endl;
delete_shared( v_ptr );
std::cerr << "--------delete b_ptr--------" << std::endl;
delete_shared( b_ptr );
std::cerr << "--------delete vbase_ptr--------" << std::endl;
delete_shared( vbase_ptr );
std::cerr << "--------delete base_ptr--------" << std::endl;
delete_shared( base_ptr );
}

news_group> a.out
--------delete v_ptr--------
V::~V
VDerived::~VDerived
VBase::~VBase
--------delete b_ptr--------
B::~B
Derived::~Derived
Base::~Base
--------delete vbase_ptr--------
V::~V
VDerived::~VDerived
VBase::~VBase
--------delete base_ptr--------
B::~B
Derived::~Derived
Base::~Base

Do you see now?

It's not uncommon in Java to have static member factory methods. It does
have the advantage of being able to access private members of the
constructed object without declaring a friend.

You can do the same in C++.


Best

Kai-Uwe Bux
 
E

Earl Purple

Kai-Uwe Bux quoted:[ I put the quoted code in here rather than have "top-posting" ]
Where did you get the idea that you are not supposed to derive from X? The
whole point of the snippet is to demonstrate that you can derive from X and
use a shared_ptr<X> as the return type of a factory. The class X_impl below
derives from X.

What may surprise you is this: shared_ptr<X> using the default deleter will
call the correct destructor (~X_impl in the example) regardless of whether
~X it was declared virtual.

It surprises me. Are you sure? Are you sure the code will even compile?
It might work with a custom deleter though, although even with that I'm
not sure. However create first a shared_ptr< x_impl > then creating a

The construction though would have to be shared_ptr< X_impl > so
createX would work thus:

shared_ptr< X > createX()
{
shared_ptr<X_impl> px(new X_impl); // creates the correct deleter
return shared_ptr< X >( px ); // copy constructor copies the
deleter from the original
}

This is at a point where X_impl is complete anyway (or you couldn't
call new on it).
 
K

Kai-Uwe Bux

Earl said:
Kai-Uwe Bux quoted:[ I put the quoted code in here rather than have "top-posting" ]
Where did you get the idea that you are not supposed to derive from X?
The whole point of the snippet is to demonstrate that you can derive from
X and use a shared_ptr<X> as the return type of a factory. The class
X_impl below derives from X.

What may surprise you is this: shared_ptr<X> using the default deleter
will call the correct destructor (~X_impl in the example) regardless of
whether ~X it was declared virtual.

It surprises me. Are you sure?
Yes.

Are you sure the code will even compile?
Yes.

It might work with a custom deleter though, although even with that I'm
not sure. However create first a shared_ptr< x_impl > then creating a
shared_ptr< x > from it should work.

shared_ptr<T> has a templated constructor

template < typename D >
shared_ptr ( D * );

this constructor initializes the deleter member so that ~D will be called.
Thus, if you do:

shared_ptr<T> t_ptr ( new D );

the pointer will call ~D upon destruction.


Try:

#include <tr1/memory>
#include <iostream>

struct X {

~X ( void ) {
std::cout << "~X\n";
}

};

struct Y : public X {

~Y ( void ) {
std::cout << "~Y\n";
}

};

int main ( void ) {
std::tr1::shared_ptr< X > x_ptr ( new Y );
}


Best

Kai-Uwe Bux
 

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

No members online now.

Forum statistics

Threads
473,744
Messages
2,569,483
Members
44,903
Latest member
orderPeak8CBDGummies

Latest Threads

Top