A few questions on C++

I

Ian Collins

James said:
Most of the places I've worked at do try and do a weekly build,
over the week-end, but I've worked on projects large enough that
even that required some optimization (linking on the servers,
compiling in parallel on the hundreds of workstations connected
to the network, etc.).
Surely not on modern hardware? Even something as complex as OpenSolaris
builds in well under an hour on a decent desktop.
 
P

Phlip

Ian said:
Surely not on modern hardware? Even something as complex as OpenSolaris
builds in well under an hour on a decent desktop.

We need jargon to distinguish "total build and test from scratch" from "one
incremental rebuild and retest of the changed files".

And, yes, the point of tiny little trivial programs like OpenSolaris or
Linux is so we can fill the rest of our computers up with our humongous
applications. Look at Bloomberg, for example!
 
J

James Kanze

Surely not on modern hardware? Even something as complex as
OpenSolaris builds in well under an hour on a decent desktop.

I don't know. It was some time ago. But the application was
considerably larger than an OS---most are these days. And we
needed to build it for several different configurations. And of
course, the build process involved running the unit tests for
every component.
 
K

Kai-Uwe Bux

[huge snip]
In theory. In practice, it tends to be more complicated; the
smart pointer isn't sufficient, and once you've implemented the
additional stuff, it isn't necessary. Thus, for example, in the
observer pattern, the observable normally doesn't have a
"pointer" to the observer, but a container of pointers. And
when one of the observables commits suicide, not removing the
pointer from the container will result in a memory leak.

Some fifteen years ago, when I started C++, there was a lot of
discussion (at least where I was working) about relationship
management, and a lot of effort was expended trying to find a
good generic solution, so that you didn't have to write so much
code by hand, each time around. As far as I know, however, no
good generic solution was ever found.

I was thinking of something a little more modest. Here is a proof of concept
for what I had in mind: a smart pointer that notifies everybody interested
of the destruction of the pointee. Clients can register arbitrary actions,
and change those actions later. (I apologize for the length of the code. I
got carried away:)


// a smart pointer that propagates deletion information
// ====================================================

#include <tr1/functional>
#include <cassert>
#include <functional>
#include <algorithm>
#include <map>


namespace kubux {

template < typename T >
class observing_ptr {

typedef void(notify_sig)( observing_ptr & );
typedef std::tr1::function< notify_sig > destruction_handler;
// typedef kubux::function< notify_sig > destruction_handler;

static
void delete_T_ptr ( T* ptr ) {
delete ( ptr );
}

// operator==, operator!=, and operator<
// all ignore the handler, i.e., the identity
// of the pointer does not depend on this field.
mutable destruction_handler the_handler;

observing_ptr * next;
observing_ptr * prev;

void insert_before ( observing_ptr * where ) {
next = where;
if ( next ) {
prev = next->prev;
next->prev = this;
} else {
prev = 0;
the_ptr->head = this;
}
if ( prev ) {
prev->next = this;
} else {
the_ptr->head = this;
}
}

void remove ( void ) {
if ( next ) {
next->prev = prev;
} else {
the_ptr->tail = prev;
}
if ( prev ) {
prev->next = next;
} else {
the_ptr->head = next;
}
next = 0;
prev = 0;
}

struct data {

T * t_ptr;
observing_ptr * head;
observing_ptr * tail;
unsigned long count;

data ( T * tp, observing_ptr * op )
: t_ptr ( tp )
, head ( op )
, tail ( op )
, count ( 1 )
{}

~data ( void ) {
assert( t_ptr == 0 );
}

};

data * the_ptr;

observing_ptr ( char )
: the_handler( &do_nothing )
, next ( 0 )
, prev ( 0 )
, the_ptr ( new data ( 0, this ) )
{
the_map()[ the_ptr-> t_ptr ] = the_ptr;
}

static
observing_ptr & null_ptr ( void ) {
static observing_ptr our_null ( 'a' );
return ( our_null );
}

static
std::map< T*, data* > & the_map ( void ) {
static std::map< T*, data* > table;
return ( table );
}

public:

static
void do_nothing ( observing_ptr const & ) {}

static
void set_zero ( observing_ptr & ptr ) {
observing_ptr().swap( ptr );
}

void swap ( observing_ptr & other ) {
std::swap( this->the_handler, other.the_handler );
std::swap( this->the_ptr, other.the_ptr );
std::swap( this->next, other.next );
std::swap( this->prev, other.prev );
}

observing_ptr ( T& t_ptr )
: the_handler ( &do_nothing )
, next ( 0 )
, prev ( 0 )
, the_ptr ( the_map()[ &t_ptr ] )
{
if ( ! the_ptr ) {
the_ptr = new data ( &t_ptr, this );
}
}

template < typename Handler >
observing_ptr ( T& t_ptr, Handler h )
: the_handler ( h )
, next ( 0 )
, prev ( 0 )
, the_ptr ( the_map()[ &t_ptr ] )
{
if ( ! the_ptr ) {
the_ptr = new data ( &t_ptr, this );
}
}

observing_ptr ( T* t_ptr = 0 )
: the_handler( &do_nothing )
, next ( 0 )
, prev ( 0 )
, the_ptr ( t_ptr
? new data ( t_ptr, this )
: null_ptr().the_ptr )
{
if ( ! t_ptr ) {
insert_before( the_ptr->tail );
++ the_ptr->count;
}
the_map()[ t_ptr ] = the_ptr;
}

template < typename Handler >
observing_ptr ( T* t_ptr, Handler h )
: the_handler( h )
, next ( 0 )
, prev ( 0 )
, the_ptr ( t_ptr
? new data ( t_ptr, this )
: null_ptr().the_ptr )
{
if ( ! t_ptr ) {
insert_before( the_ptr->tail );
++ the_ptr->count;
}
the_map()[ t_ptr ] = the_ptr;
}

observing_ptr ( observing_ptr const & other )
: the_handler( other.the_handler )
, the_ptr ( other.the_ptr )
{
assert( other.is_valid() );
insert_before ( the_ptr->tail );
++ the_ptr->count;
}

template < typename Handler >
observing_ptr ( observing_ptr const & other, Handler h )
: the_handler( h )
, the_ptr ( other.the_ptr )
{
assert( other.is_valid() );
insert_before( the_ptr->tail );
++ the_ptr->count;
}

observing_ptr & operator= ( observing_ptr const & other ) {
observing_ptr ( other ).swap( *this );
return ( *this );
}

~observing_ptr ( void ) {
assert( the_ptr );
remove();
--the_ptr->count;
if ( the_ptr->count == 0 ) {
delete ( the_ptr );
}
}

template < typename Deleter >
friend
void kill ( observing_ptr ptr, Deleter deleter ) {
data * the_ptr = ptr.the_ptr;
T * t_ptr = the_ptr->t_ptr;
while ( the_ptr->head ) {
the_ptr->head->the_handler( *( the_ptr->head ) );
the_ptr->head->remove();
}
the_map().erase( t_ptr );
deleter( t_ptr );
the_ptr->t_ptr = 0;
}

friend
void kill ( observing_ptr ptr ) {
kill( ptr, &delete_T_ptr );
}

void zero_handler ( void ) const {
reset_handler ( &set_zero );
}

void reset_handler ( void ) const {
the_handler = &do_nothing;
}

template < typename Handler >
void reset_handler ( Handler h ) const {
the_handler = h;
}

bool is_valid ( void ) const {
return ( the_ptr->head != 0 );
}

T & operator* ( void ) {
assert( get() );
return ( *get() );
}

T const & operator* ( void ) const {
assert( get() );
return ( *get() );
}

T * operator-> ( void ) {
assert( get() );
return ( get() );
}

T const * operator-> ( void ) const {
assert( get() );
return ( get() );
}

T * get ( void ) {
assert( the_ptr );
assert( is_valid() );
return ( the_ptr->t_ptr );
}

T const * get ( void ) const {
assert( the_ptr );
assert( is_valid() );
return ( the_ptr->t_ptr );
}

friend
bool operator== ( observing_ptr const & lhs,
observing_ptr const & rhs ) {
return ( lhs.get() == rhs.get() );
}

friend
bool operator!= ( observing_ptr const & lhs,
observing_ptr const & rhs ) {
return ( lhs.get() != rhs.get() );
}

friend
bool operator< ( observing_ptr const & lhs,
observing_ptr const & rhs ) {
return ( lhs.the_ptr < rhs.the_ptr );
}

};

template < typename T >
void swap ( observing_ptr<T> & lhs, observing_ptr<T> & rhs ) {
lhs.swap( rhs );
}


template < typename T >
struct suicide_policy {

void die ( void ) {
kill( observable_this() );
}

private:

observing_ptr<T>
observable_this ( void ) const {
return ( observing_ptr<T>
( *const_cast<T*>
( static_cast<T const *>( this ) ) ) );
}

};

} // namespace kubux


// a little test
// =============

#include <set>
#include <cassert>

struct X : public kubux::suicide_policy<X> {

X ( void ) {}

virtual
std::string id ( void ) const {
return ( "X" );
}

virtual
~X ( void ) {}
};

struct Y : public X {

Y ( void )
: X ()
{}

virtual
std::string id ( void ) const {
return ( "Y" );
}

};

typedef kubux::eek:bserving_ptr<X> X_ptr;

class SomeMonitoringClass {

typedef std::set< X_ptr > X_ptr_set;

struct Remover {

X_ptr_set * set_ptr;

Remover ( X_ptr_set * arg )
: set_ptr ( arg )
{}

void operator() ( X_ptr & op ) const {
set_ptr->erase( op );
}

};

X_ptr_set the_X_ptrs;

public:

void insert ( X_ptr x_ptr ) {
X_ptr_set::iterator result = the_X_ptrs.insert( x_ptr ).first;
result->reset_handler( Remover( &the_X_ptrs ) );
}

X_ptr_set::size_type size ( void ) const {
return ( the_X_ptrs.size() );
}

};


int main ( void ) {
{
assert( X_ptr() == X_ptr() );
}
{
SomeMonitoringClass monitor;

X_ptr x_ptr_1 ( new X );
X_ptr x_ptr_2 ( new X );
X_ptr x_ptr_3 ( x_ptr_1, X_ptr::set_zero );
X_ptr x_ptr_4 ( x_ptr_1 );

assert( ! ( x_ptr_1 == x_ptr_2 ) );
assert( x_ptr_1 != x_ptr_2 );
assert( x_ptr_1 < x_ptr_2 || x_ptr_2 < x_ptr_1 );
assert( x_ptr_1 == x_ptr_3 );
assert( ! ( x_ptr_1 != x_ptr_3 ) );
assert( ! ( x_ptr_1 < x_ptr_3 || x_ptr_3 < x_ptr_1 ) );
assert( x_ptr_1 == x_ptr_4 );
assert( ! ( x_ptr_1 != x_ptr_4 ) );
assert( ! ( x_ptr_1 < x_ptr_4 || x_ptr_4 < x_ptr_1 ) );
assert( x_ptr_3 == x_ptr_4 );
assert( ! ( x_ptr_3 != x_ptr_4 ) );
assert( ! ( x_ptr_3 < x_ptr_4 || x_ptr_4 < x_ptr_3 ) );

monitor.insert( x_ptr_1 );
monitor.insert( x_ptr_2 );
monitor.insert( x_ptr_3 );
monitor.insert( x_ptr_4 );

assert( monitor.size() == 2 );
kill( x_ptr_1 );
assert( monitor.size() == 1 );
assert( x_ptr_3.is_valid() );
assert( x_ptr_3 == X_ptr() );
assert( ! x_ptr_4.is_valid() );
x_ptr_2->die();
assert( ! x_ptr_2.is_valid() );
assert( monitor.size() == 0 );
}
}


// end of file

As you can see, you still have to write the code for removing the pointer
from the container. But the code is not spread out in the program. Instead
one can read the code for the monitor class and know that the pointers in
the container will all be valid at any given moment.


However, after writing all that, it occurred to me that this is probably a
solution in search of a problem (to my defense, there have been post in the
past where people asked for something like this or presented their own
solution). I realized that the death of an object is just one event of
many, and you probably have a need to spread notifications for many other
types of events, too. So once that scaffolding is in place, you can use it
for death notes, as well. (Question: is this suspicion correct?)

So, it is entirely possible that all of the above can be replaced by
something small and flexible like this:


#include <tr1/functional>
#include <list>

class command_bag {

typedef std::tr1::function<void(void)> command;
typedef std::list< command > command_list;

command_list the_list;

public:

typedef command_list::iterator removal_ticket;

template < typename Command >
removal_ticket register_command ( Command c ) {
the_list.push_front( command( c ) );
return ( the_list.begin() );
}

void remove_command ( removal_ticket t ) {
the_list.erase( t );
}

void execute ( void ) const {
for ( command_list::const_iterator iter = the_list.begin();
iter != the_list.end(); ++iter ) {
(*iter)();
}
}

};

#include <iostream>
#include <string>

struct PrintCommand {

std::string banner;

PrintCommand ( std::string str )
: banner ( str )
{}

void operator() ( void ) const {
std::cout << banner << '\n';
}

};


int main ( void ) {
command_bag bag;
bag.register_command( PrintCommand( "hello world!" ) );
bag.register_command( PrintCommand( "hello world!" ) );
command_bag::removal_ticket one =
bag.register_command( PrintCommand( "hello" ) );
bag.register_command( PrintCommand( "hello world!" ) );
bag.register_command( PrintCommand( "hello world!" ) );
bag.remove_command( one );
bag.execute();
}


Oh well, I guess, I should stick to my value objects. Entity objects are too
troublesome :)

[...]
I don't really like smart pointers there either.

It's not that I don't like them; when they are appropriate, I
don't hesitate using them. I don't like them being presented as
a silver bullet, as they so often are. Nor do I like the fact
that many people are suggesting that you should never use raw
pointers, or that there is one magical smart pointer
(boost::shared_ptr) that will solve all (or even most) of your
problems.
However, they are really handy in getting a prototype up and
running, which is a good thing during the design phase when
you are experimenting with the interface and whip up the
initial test cases. When the design is stabilizing, I tend to
first replace smart pointers (and raw pointers) by pointer
wrappers that support hunting double deletion and memory
leaks, and finally by pointer wrappers that wrap new and
delete and provide hooks for an allocator to be specified by
the client code.

Interesting. I use external tools for much of this. For memory
management within the process, I usually use the Boehm
collector; why should I have to worry about which smart pointer
to use, or how to break a cycle, *IF* the object doesn't have an
explicit lifetime (i.e. if there is no explicit behavior
associated with its ceasing to exist). For the rest, I'll use
Purify or valgrind, or even my own debugging new/delete.

I started adding smart pointers to my code base when I decided that all my
container like classes should use allocators. Now, I have pointer wrappers
that use an allocator as a policy and encapsulate creation and destruction
of the pointee (classical smart pointers only do destruction). It turns out
that this is a very flexible design that can make very strong guarantees
(since construction is under the control of the smart pointer).

Very good point. Writing good, generic templates is difficult,
and typically, you do end up violating a number of rules that
would apply elsewhere.

Strongly agreed. For instance inheritance is used for entirely different
purposes in generic programming.

The results are often much more difficult to understand, as well.

Actually, that is a matter of habit. I would probably find a typical piece
of your code base impenetrable because I would need to understand the
relations and dependencies of all those entity objects whose life times
need to be managed carefully. You are using a different set of idioms
altogether. That just requires a different style of thinking, but I do not
think that either style is inherently more difficult than the other.

Yet another reason why a lot
of companies don't like templates at the application level. (In
most of my applications, the majority of the application
programmers are domain specialists, and not C++ or software
engineering specialists. My role in such projects is usually to
handle such low level or generic stuff in such a way that the
application programmers don't have to worry about it.)

I guess that is a reasonable division of labor.

[snip]
In a very real sense, there's something scary about any delete;
you have to be very sure that no one else is using the object,
and that all concerned parties are notified. Delete this is
really no different in this respect. And most of the time I've
seen people try to avoid it, per se, they end up just hiding
it---obfuscating the potential problems; there is no fundamental
difference between "delete this" and
"ObjectManager::instance().removeObject( this )", except that it
is far more explicit in the first case that the object won't
exist after the statement.

This remark led to the addition of the die() method and the public_suicide<>
policy in the above code. You definitely have a point that there should be
a concise and unmistakable textual representation of object-death in the
program code.


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,776
Messages
2,569,603
Members
45,197
Latest member
ScottChare

Latest Threads

Top