Re: To use or not to use smart pointers?
James Kanze wrote:
On Jul 18, 11:02 pm, Kai-Uwe Bux <jkherci...@gmx.net> wrote:
[...]
(and also should carry the burden of proving the correctness
for the code base resulting from a design decision in favor of
raw pointers).
That is, of course, the key part. Systematic use of shared_ptr,
where not appropriate, makes it very difficult to prove that the
code is correct.
Pointers in C++ are heavily overloaded. They offer two main
features at once, namely (a) decoupling life-time from scope
and (b) support for polymorphism. If you want the later, you
are inviting upon you the perrils of the former.
That pointer and pointee have different life-times introduces
a global state into the program and arguing correctness becomes
much more of a hassle: every new() has to match with one and
only one delete() along each path of execution. Exceptions can
divert the execution path at any moment to an unknown location,
and client supplied types in templates make it unpredictable
what will throw and what will be thrown. That makes life very
hard.
A smart pointer like shared_ptr<> also requires global
knowledge in at least two ways: (a) we need to ensure that
no cycles occur and (b) when one relies on side-effects of
a destructor, sometimes one needs to argue that the last
pointer owning a given pointee is going out of scope.
For me, the lesson is that one needs a set of idioms and tools
that allow us to reduce code complexity and restore the ability
to argue correctness of code locally (by looking at, say, 20
_consecutive_ lines of code at a time). Thus, I tend to wrap
pointers within classes. Smart pointers are just readily packaged
idioms that come up often in this context. For these reasons, smart
pointers form an intermediate layer in my library. They are used
to build other components but rarely used directly. Let me
illustrate this with a few examples.
Example 1 [statefull function objects]
=========
The standard algorithms take function objects by value
and (with the notable exception of for_each) are allowed
to copy those object around. Thus, I found something like
this usefull at times:
template < typename Func >
class unary_f_ref
: std::unary_function< typename Func::argument_type,
typename Func::result_type >
{
std::tr1::shared_ptr<Func> the_func;
public:
unary_f_ref ( Func const & f )
: the_func ( new Func ( f ) )
{}
typename Func::result_type
operator() ( typename Func::argument_type arg ) {
return ( (*the_func)( arg ) );
}
Func & get ( void ) {
return ( *the_func );
}
Func const & get ( void ) const {
return ( *the_func );
}
};
Similar trickery can be applied to reduce the cost of
copying for large objects, in which case reference countng
becomes an invisible performance hack.
Note that shared_ptr<> is actually overkill in this context.
It's support for polymorphism, incomplete types, and a custom
deleter is not needed. A simple intrusive reference counting
smart pointer (not working with incomplete types) fits this
bill neatly.
Example 2 [deep copy pointers]
=========
C++ has value semantics. However, variables are not polymorphic.
You have to use a pointer to support polymorphism. Deep copy
pointers come as close as possible to polymorphic variables
as you can get in C++ (if only we could overload the dod-operator).
A particular example is tr1::function<>, which under the hood
is a deep copy pointer that forwards the function interface to
the pointee.
Example 3 [ownership models]
=========
Smart pointers do not manage resources magically for you, but
sometimes their semantics makes enforcing an ownership model
easier. For instance, a shared resource could be realized like
this:
struct A {};
class SharedResource {
// could be polymorphic
struct Descriptor {
Descriptor ( A a )
{}
void close ( void ) {}
// could be virtual
int some_method ( int ) {
return ( 1 );
}
};
std::tr1::shared_ptr< Descriptor > the_ptr;
public:
~SharedResource ( void ) {
assert( ! is_open() );
}
enum mode { ok, busy, error_1, error_2 };
// you can only open a resource if you don't have one
mode open ( A a ) {
if ( ! is_open() ) {
try {
Descriptor * ptr = new Descriptor ( a );
the_ptr.reset( ptr ); // hopefully strongly safe
}
catch( ... ) {
// return whatever is needed
}
return ( ok );
}
return ( busy );
}
// only the last owner can close the resource
bool close ( void ) {
if ( the_ptr.unique() ) {
the_ptr->close(); // may throw
the_ptr.reset();
return ( true );
}
return ( false );
}
// the last owner cannot disown:
bool disown ( void ) {
if ( is_shared() ) {
the_ptr.reset();
return ( true );
}
return ( false );
}
bool is_open ( void ) const {
return ( the_ptr );
}
bool is_shared ( void ) const {
return ( is_open() && ( ! the_ptr.unique() ) );
}
// forward interface
// =================
int some_method ( int arg ) {
assert( is_open() );
the_ptr->some_method( arg );
}
};
Note that the shared_ptr does not show in the interface
On the other hand, there are sometimes reasons to use a
smart pointer directly, although I always feel a little
uneasy. Example:
#include <memory>
class UserCommand {
public:
// ...
virtual
~UserCommand ( void ) {}
};
// magic source of commands.
// in real life, this is polymorphic.
UserCommand * getUserInput ( void ) {
return new UserCommand;
}
class MessageHandler {
public:
// ...
virtual
void operator() ( UserCommand const & cmd,
bool & do_terminate );
// UserCommand is polymorphic
virtual
~MessageHandler ( void ) {}
};
struct MyException {};
void event_loop ( MessageHandler & the_handler ) {
// MessageHandler is polymorphic
try {
bool last_run = false;
do {
// neeed for pointer because of polymorphism:
UserCommand * the_command = getUserInput();
the_handler( *the_command, last_run );
} while ( last_run );
}
catch ( MyException const & e ) {
if ( false ) {
// placeholder for things that can be
// handled here
} else {
throw( e );
}
}
}
The event_loop as written is buggy. If the line
the_handler( *the_command, last_run );
throws, we leak memory. One can use std::auto_ptr<> to
rectify things:
void event_loop ( MessageHandler & the_handler ) {
// MessageHandler is polymorphic
try {
bool last_run = false;
do {
// neeed for pointer because of polymorphism:
std::auto_ptr< UserCommand > the_command ( getUserInput() );
the_handler ( *the_command, last_run );
} while ( last_run );
}
catch ( MyException const & e ) {
if ( false ) {
// placeholder for things that can be
// handled here
} else {
throw( e );
}
}
}
Best
Kai-Uwe Bu