Guru of the Week #8: Solution

106 views
Skip to first unread message

Herb Sutter

unread,
Apr 17, 1997, 3:00:00 AM4/17/97
to

.--------------------------------------------------------------------.
| Guru of the Week problems and solutions are posted weekly on |
| news:comp.lang.c++.moderated. For past problems and solutions, |
| see the GotW archive at http://www.cntc.com. |
| Is there a topic you'd like to see covered? mailto:he...@cntc.com |
`--------------------------------------------------------------------'

GotW #8: Exception Safety (Two-Week Challenge)

Difficulty: 9 / 10


IMPORTANT NOTE: I do not claim that this solution actually meets my
original requirements. In fact, I don't have a compiler than can
compile it! While I've addressed as many of the interactions as
I can think of, a major point of this exercise was to demonstrate
that writing exception-safe code requires care.

See also <http://www.awl.com/cp/mec++-cargill.html> for a copy of
Tom Cargill's excellent article, "Exception Handling: A False Sense
of Security" (C++ Report, vol. 9 no. 6, Nov-Dec 1994). He shows
that EH is tricky, but please note that his article was probably
not meant to dismiss EH in general and people shouldn't get too
superstitious about using exceptions. Just be aware, and drive
with care.

Last note: To keep this solution simpler, I've decided not to
demonstrate the base class technique for exception-safe resource
ownership. I invite Dave Abrahams (or others) to follow up to
this solution and demonstrate this very effective technique.


To recap, here's the required interface:

> template <class T> // T must be default-constructible and assignable
> class Stack
> {
> public:
> Stack();
> ~Stack();
> Stack(const Stack&);
> Stack& operator=(const Stack&);
>
> unsigned Count();
> void Push(const T&);
> T Pop();
>
> private:
> T* v_; // pointer to a memory area big
> // enough for 'vsize_' T objects
> unsigned vsize_; // the size of the 'v_' area
> unsigned vused_; // the number of T's actually
> // used in the 'v_' area
> };


Now for the implementations. One requirement we will place on T is
T dtors must not throw. If dtors may throw, many operations are
difficult or impossible to implement safely.


//----- DEFAULT CTOR ----------------------------------------------
template<class T>
Stack<T>::Stack()
: v_(new T[10]), // default allocation
vsize_(10),
vused_(0) // nothing used yet
{
// if we got here, the construction was okay
}

//----- COPY CTOR -------------------------------------------------
template<class T>
Stack<T>::Stack( const Stack<T>& other )
: v_(0), // nothing allocated or used yet
vsize_(other.vsize_),
vused_(other.vused_)
{
v_ = NewCopy( other.v_, other.vsize_, other.vsize_ );
// if we got here, the copy construction was okay
}

//----- COPY ASSIGNMENT -------------------------------------------
template<class T>
Stack<T>& Stack<T>::operator=( const Stack<T>& other )
{
if( this != &other )
{
T* v_new = NewCopy( other.v_, other.vsize_, other.vsize_ );
// if we got here, the allocation and copy were okay

delete[] v_;
// note that this cannot throw, since T dtors cannot
// throw and ::operator delete[] is declared as throw()

v_ = v_new;
vsize_ = other.vsize_;
vused_ = other.vused_;
}

return *this; // safe, no copy involved
}

//----- DTOR ------------------------------------------------------
template<class T>
Stack<T>::~Stack()
{
delete[] v_; // again, this can't throw
}

//----- COUNT -----------------------------------------------------
template<class T>
unsigned Stack<T>::Count()
{
return vused_; // it's just a builtin, nothing can go wrong
}

//----- PUSH ------------------------------------------------------
template<class T>
void Stack<T>::Push( const T& t )
{
if( vused_ == vsize_ ) // grow if necessary
{
unsigned vsize_new = (vsize_+1)*2; // grow factor
T* v_new = NewCopy( v_, vsize_, vsize_new );
// if we got here, the allocation and copy were okay

delete[] v_; // again, this can't throw
v_ = v_new;
vsize_ = vsize_new;
}

v_[vused_] = t; // if this copy throws, the increment
++vused_; // isn't done and the state is unchanged
}

//----- POP -------------------------------------------------------
template<class T>
T Stack<T>::Pop()
{
T result;
if( vused_ > 0)
{
result = v_[vused_-1]; // if this copy throws, the
--vused_; // decrement isn't done and
} // the state is unchanged
return result;
}

//
// NOTE: As alert reader Wil Evers was the first to point out,
// "As defined in the challenge, Pop() forces its users to write
// exception-unsafe code, which first causes a side effect
// (popping an element off the stack) and then lets an exception
// escape (copying the return value into [the caller's
// destination object])."
//
// This points out that one reason writing exception-safe
// code is so hard is that it affects not only your
// implementations, but also your interfaces! Some
// interfaces, like this one, can't be implemented in a
// fully exception-safe manner.
//
// One way to correct this one is to respecify the function as
// "void Stack<T>::Pop( T& result)". This way we can know
// the copy to the result has actually succeeded before we
// change the state of the stack. For example, here's an
// alternative version of Pop() that's more exception-safe:
//
template<class T>
void Stack<T>::Pop( T& result )
{
if( vused_ > 0)
{
result = v_[vused_-1]; // if this copy throws, the
--vused_; // decrement isn't done and
} // the state is unchanged
}
//
// Alternatively, we may let Pop() return void and provide a
// Front() member function to access the topmost object.
//


//----- HELPER FUNCTION -------------------------------------------
// When we want to make a (possibly larger) copy of a T buffer,
// this function will allocate the new buffer and copy over the
// original elements. If any exceptions are encountered, the
// function releases all temporary resources and propagates
// the exception so that nothing is leaked.
//
template<class T>
T* NewCopy( const T* src, unsigned srcsize, unsigned destsize )
{
destsize = max( srcsize, destsize ); // basic parm check
T* dest = new T[destsize];
// if we got here, the allocation/ctors were okay

try
{
copy( src, src+srcsize, dest );
}
catch(...)
{
delete[] dest;
throw; // rethrow the original exception
}
// if we got here, the copy was okay

return dest;
}


___________________________________________________________________

Now for the bonus questions:


>1. Are the standard library containers exception-safe or
> exception-neutral, according to the current draft?

This is presently unspecified. Lately there has been some
discussion within the committee about whether to provide
either weak ("container is always destructible") or strong
("all container operations have commit-or-rollback semantics")
exception safety guarantees. As Dave Abrahams has pointed out
in that discussion, and since then in email, often the strong
guarantee "comes along for free" if you implement the weak
guarantee. This is the case with several operations above.


>2. Should containers be exception-neutral? Why or why not?
> What are the tradeoffs?

Some containers have operations with unavoidable space tradeoffs
if they are to be made exception-neutral. Exception-neutrality
is a Good Thing in itself, but may not be practical when
implementing the strong guarantee requires much more space or
time than does implementing the weak guarantee. Often a good
compromise is to document which T operations are expected not to
throw and then guarantee exception-neutrality based on
conformance to those assumptions.


>3. Should containers use exception specifications? For example,
> should we declare "Stack::Stack() throw( bad_alloc );"?

No, since it is unknown in advance which T operations might throw
or what they might throw.

Notice that some container operations (e.g., Count()) simply
return a scalar and are known not to throw. While it's possible
to declare these with "throw()", there are two possible reasons
why you wouldn't: first, it limits you in the future in case you
want to change the underlying implementation to a form which
could throw; and second, exception specifications incur a
performance overhead whether an exception is thrown or not. For
widely-used operations, it may be better not to use exception
specifications to avoid this overhead.


>CHALLENGE: With many current compilers, using "try" and "catch"
>often adds unnecessary overhead to your programs, which would be
>nice to avoid in this kind of low-level reusable container. Can
>you implement all Stack member functions as required without
>ever using "try" or "catch"?

Yes, because we only care about catching "...". In general, code
of the form

try { TryCode(); } catch(...) { CatchCode(parms); throw; }

can be rewritten as

struct Janitor {
Janitor(Parms p) : pa(p) {}
~Janitor() { if uncaught_exception() CatchCode(pa); }
Parms pa;
};

{
Janitor j(parms); // j is destroyed both if TryCode()
// succeeds and if it throws
TryCode();
}


Our solution uses try/catch only in the NewCopy function,
so let's illustrate this technique by rewriting NewCopy:

template<class T>
T* NewCopy( const T* src, unsigned srcsize, unsigned destsize )
{
destsize = max( srcsize, destsize ); // basic parm check

struct Janitor {
Janitor( T* p ) : pa(p) {}
~Janitor() { if( uncaught_exception() ) delete[] pa; }
T* pa;
};

T* dest = new T[destsize];
// if we got here, the allocation/ctors were okay

Janitor j(dest);
copy( src, src+srcsize, dest );
// if we got here, the copy was okay... otherwise, j
// was destroyed during stack unwinding and will handle
// the cleanup of dest to avoid leaking memory

return dest;
}


Having said that, I've now talked to several people who've done
empirical speed tests. In the case when no exception occurs,
try/catch is usually much faster, and can be expected to continue
to be faster. However, it is still important to know about this
kind of technique because it often yields more elegant and
maintainable code, and because some current compilers still
produce extremely inefficient code for try/catch in both the
exceptional and exception-free code paths.


---
Herb Sutter (mailto:he...@cntc.com)

Current Network Technologies Corp. (http://www.cntc.com)
2695 North Sheridan Way, Suite 150, Mississauga ON Canada L5K 2N6
Tel 416-805-9088 Fax 905-822-3824

[ Send an empty e-mail to c++-...@netlab.cs.rpi.edu for info ]
[ about comp.lang.c++.moderated. First time posters: do this! ]


Herb Sutter

unread,
Apr 18, 1997, 3:00:00 AM4/18/97
to

he...@cntc.com (Herb Sutter) wrote:
>Yes, because we only care about catching "...". In general, code
>of the form
>
> try { TryCode(); } catch(...) { CatchCode(parms); throw; }
>
>can be rewritten as
>
> struct Janitor {
> Janitor(Parms p) : pa(p) {}
> ~Janitor() { if uncaught_exception() CatchCode(pa); }
> Parms pa;
> };
>
> {
> Janitor j(parms); // j is destroyed both if TryCode()
> // succeeds and if it throws
> TryCode();
> }

As Wil Evers pointed out in the other thread, this won't correctly detect
whether or not the TryCode() succeeded if an exception was already active
before entering the block. The generally reliable solution is still to use a
sentinel:

struct Janitor {
Janitor(Parms p) : pa(p), ok(false) {}
~Janitor() { if( !ok ) CatchCode(pa); }
void Ok() { ok = true; }
Parms pa;
bool ok;
};

{
Janitor j(parms); // j is destroyed both if TryCode()
// succeeds and if it throws
TryCode();

j.Ok();

Dave Abrahams

unread,
Apr 23, 1997, 3:00:00 AM4/23/97
to


Herb Sutter <he...@cntc.com> wrote in article
<5j4rd0$a...@netlab.cs.rpi.edu>...


>
> IMPORTANT NOTE: I do not claim that this solution actually meets my
> original requirements. In fact, I don't have a compiler than can
> compile it! While I've addressed as many of the interactions as
> I can think of, a major point of this exercise was to demonstrate
> that writing exception-safe code requires care.

This solution does meet the requirements, and I've tested it with an
automated test suite just to be certain. Others have correctly pointed out
that Pop() can modify the stack and still fail to produce a return value;
this is a limitation of the interface specified in the exercise. Aside from
that, all mutating member functions exhibit the "strong guarantee": that
they either succeed or the Stack is left unchanged. My implementation takes
advantage of three templates from the standard library: construct, destroy,
and swap. These are very simple, and anyone with an interest should be able
to inspect an STL implementation if they want to understand what they do.

#include <algobase.h>
#include <algo.h>

template <class T>
class StackBase
{
protected:
StackBase() : v_(0), vsize_(0), vused_(0) {}

StackBase( unsigned n )
: v_( (T*)new char[sizeof(T)*n] ),
vsize_(n),
vused_(0)
{}

~StackBase()
{
destroy( v_, v_+vused_ );
delete[] (char*)v_;
}

void swap( StackBase& rhs )
{
std::swap( v_, rhs.v_ );
std::swap( vsize_, rhs.vsize_ );
std::swap( vused_, rhs.vused_ );


}

T* v_; // pointer to a memory area big
// enough for 'vsize_' T objects
unsigned vsize_; // the size of the 'v_' area
unsigned vused_; // the number of T's actually
// used in the 'v_' area
};

template <class T>
// T must have default ctor and copy assignment
// T's dtor may not leak exceptions
class Stack : private StackBase<T>
{
public:
Stack() {}
~Stack() {}
Stack(const Stack& rhs)
: StackBase<T>( rhs.Count() )
{
while ( Count() < rhs.Count() )
Push( rhs.v_[ Count() ] );
}

Stack& operator=(const Stack& rhs)
{
Stack tmp(rhs);
swap(tmp);
}

unsigned Count() const // returns number of T's in the stack
{
return vused_;
}

void Push(const T& t)
{
if ( vsize_ == vused_ )
{
Stack tmp( 2*vsize_+1 );
while ( tmp.Count() < Count() )
tmp.Push( v_[ tmp.Count() ] );
tmp.Push( t );
swap( tmp );
}
else
{
construct( v_+Count(), t );
vused_++;
}
}

T Pop() // if empty, returns default-constructed T
{
if ( Count() == 0 )
return T();
--vused_;
T result( v_[vused_] );
destroy( v_ + vused_ );
return result;
}

private:
Stack( unsigned n ) : StackBase<T>( n ) {}
};


> See also <http://www.awl.com/cp/mec++-cargill.html> for a copy of
> Tom Cargill's excellent article, "Exception Handling: A False Sense
> of Security" (C++ Report, vol. 9 no. 6, Nov-Dec 1994). He shows
> that EH is tricky, but please note that his article was probably
> not meant to dismiss EH in general and people shouldn't get too
> superstitious about using exceptions. Just be aware, and drive
> with care.

I want to make a stronger caution regarding this article. It is well
thought-out, but because it doesn't present a solution it has
(unintentionally, I'm sure) caused many people to give up on
exception-safety, especially in templates. I can't tell you how many times
people have quoted that article to me in defense of the position that
writing exception-safe templates may be simply impossible. THIS IS UNTRUE.
I wish Tom would amend his article to that effect, and present a working
solution. This would do a great service to the C++ community.

> Last note: To keep this solution simpler, I've decided not to
> demonstrate the base class technique for exception-safe resource
> ownership. I invite Dave Abrahams (or others) to follow up to
> this solution and demonstrate this very effective technique.

The base class technique is demonstrated above. Its main advantage is that
the cleanup code for all of the Stack's constructors can be handled by its
base class' destructor at no cost to efficiency. Any number of properly
written constructors can be added to Stack's interface without adding code
bloat for exception cleanup.

> >1. Are the standard library containers exception-safe or
> > exception-neutral, according to the current draft?
>
> This is presently unspecified. Lately there has been some
> discussion within the committee about whether to provide
> either weak ("container is always destructible") or strong
> ("all container operations have commit-or-rollback semantics")
> exception safety guarantees. As Dave Abrahams has pointed out
> in that discussion, and since then in email, often the strong
> guarantee "comes along for free" if you implement the weak
> guarantee. This is the case with several operations above.

There is now an exception-neutral implementation of the STL available at:
<http://www.ipmce.su/people/fbp/stl/stlport.html>. You can find preliminary
documentation of the exception-handling work at
<http://www.ipmce.su/people/fbp/stl/eh_contract.html>. This contains a more
complete description of the basic (what Herb calls "weak") and strong
guarantees that this version of the library provides. By the way, the basic
library guarantees go well beyond containers beind destructible. They also
say that containers remain valid and that no resources are leaked. Please
note that this is, to my knowledge, the only exception-neutral STL
implementation at this time. If you are using some other version of the
STL, you are probably getting no protection from exceptions.

> >2. Should containers be exception-neutral? Why or why not?
> > What are the tradeoffs?
>
> Some containers have operations with unavoidable space tradeoffs
> if they are to be made exception-neutral. Exception-neutrality
> is a Good Thing in itself, but may not be practical when
> implementing the strong guarantee requires much more space or
> time than does implementing the weak guarantee. Often a good
> compromise is to document which T operations are expected not to
> throw and then guarantee exception-neutrality based on
> conformance to those assumptions.

I don't think you answered your first question here. I say YES, they should
be exception-neutral. Otherwise, their usefulness is unreasonably limited.
Who wants a library where vector<string> will crash when used after an
exception is thrown?

The tradeoffs depend on the level of safety being provided. Strong
guarantees could have been provided with no change in runtime order (i.e.
operations that were linear in the number of elements remained linear in
the number of elements) for all mutating container operations, but there
would have been a significant change in the constant factor of some
operations. For example, inserting into the middle of a vector would have
become twice as slow. This was considered by many to be unacceptable. After
some consideration, I decided it was probably enough to give the basic
guarantee for all operations and to carefully document which operations
became "strongly guaranteed" automatically, so that people could use those
operations when appropriate.



> >CHALLENGE: With many current compilers, using "try" and "catch"
> >often adds unnecessary overhead to your programs, which would be
> >nice to avoid in this kind of low-level reusable container. Can
> >you implement all Stack member functions as required without
> >ever using "try" or "catch"?
>

> Having said that, I've now talked to several people who've done
> empirical speed tests. In the case when no exception occurs,
> try/catch is usually much faster, and can be expected to continue
> to be faster.

I've only looked at one compiler that incurs a function call for a try
block (Borland 5.01), and I think it's important to point out that IT SEEMS
TO INCUR THE SAME OVERHEAD FOR EACH AUTOMATIC (stack-based) OBJECT which
must be destroyed during exception unwinding. The main factor that makes
try/catch more efficient in most cases is that cleanup objects usually
store references or pointers to variables that could otherwise be put in
registers. So if you really care about speed, you'll usually use try/catch.
On the other hand, code bloat will also slow you down eventually because of
cache misses. If you look at the exception-safety work in the
aforementioned STL, you'll notice that a variety of techniques have been
used. Many try/catch blocks have been eliminated by using the base class
technique, and in a few cases cleanup objects were used. This was done

>However, it is still important to know about this
> kind of technique because it often yields more elegant and
> maintainable code, and because some current compilers still
> produce extremely inefficient code for try/catch in both the
> exceptional and exception-free code paths.

Absolutely. Writing correct exception-safe code is a bit harder than
writing correct code where no errors can occur. We need all the help we can
get as programmers, and cleanup objects of various kinds can provide the
structure neccessary to make it harder to make mistakes. Remember that 90%
of the time is spent in 10% of your code. Never optimize prematurely!

-Dave Abrahams
Mark of the Unicorn, Inc.

P.S. If you're working with Borland 5.01/5.02 and exceptions, remember that
you must turn off inlining and register-based parameter passing or your
exception code won't work. Borland generates incorrect code for with these
options enabled.

James Kanze

unread,
Apr 25, 1997, 3:00:00 AM4/25/97
to

"Dave Abrahams" <abra...@motu.com> writes:

|> Herb Sutter <he...@cntc.com> wrote in article
|> <5j4rd0$a...@netlab.cs.rpi.edu>...
|> >
|> > IMPORTANT NOTE: I do not claim that this solution actually meets my
|> > original requirements. In fact, I don't have a compiler than can
|> > compile it! While I've addressed as many of the interactions as
|> > I can think of, a major point of this exercise was to demonstrate
|> > that writing exception-safe code requires care.
|>
|> This solution does meet the requirements, and I've tested it with an
|> automated test suite just to be certain. Others have correctly pointed out
|> that Pop() can modify the stack and still fail to produce a return value;
|> this is a limitation of the interface specified in the exercise. Aside from
|> that, all mutating member functions exhibit the "strong guarantee": that
|> they either succeed or the Stack is left unchanged.

Except that I think that Pop may cause an object to never be destructed,
which is a memory leak (or worse) in potential.

|> My implementation takes
|> advantage of three templates from the standard library: construct, destroy,
|> and swap. These are very simple, and anyone with an interest should be able
|> to inspect an STL implementation if they want to understand what they do.
|>
|> #include <algobase.h>
|> #include <algo.h>
|>
|> template <class T>
|> class StackBase
|> {
|> protected:
|> StackBase() : v_(0), vsize_(0), vused_(0) {}
|>
|> StackBase( unsigned n )
|> : v_( (T*)new char[sizeof(T)*n] ),

Just a nit, but the expression "new char[...]" is not guaranteed to
return memory sufficiently aligned. Use "::operator new( ... )"
instead. The probability of this not working is admittedly very low,
but the explicit call of the operator new function is guaranteed. It
also has the advantage, IMHO, of indicating that you are allocating raw
memory, and not an array of characters.

What happens if the copy constructor throws here? Methinks that an
object has been logically removed from the stack (vused_ was
decremented), but has not been destructed. Methinks, in fact, that it
never will be destructed, and that any resources it uses are lost and
gone forever.

|> destroy( v_ + vused_ );
|> return result;
|> }
|>
|> private:
|> Stack( unsigned n ) : StackBase<T>( n ) {}
|> };

|> > Last note: To keep this solution simpler, I've decided not to


|> > demonstrate the base class technique for exception-safe resource
|> > ownership. I invite Dave Abrahams (or others) to follow up to
|> > this solution and demonstrate this very effective technique.
|>
|> The base class technique is demonstrated above. Its main advantage is that
|> the cleanup code for all of the Stack's constructors can be handled by its
|> base class' destructor at no cost to efficiency. Any number of properly
|> written constructors can be added to Stack's interface without adding code
|> bloat for exception cleanup.

One thing is bothering me: why inheritance rather than membership. Is
it only to simplify access to the data members, or is there some other
underlying reason?

Concerning exceptions in general: it is interesting that when someone in
favor of exceptions posts actual code, it isn't exception safe. Now, I
wouldn't say that exception safe code is impossible. The correction in
the above case is trivial, for example. But most people (certainly
including myself) are not atuned to the type of problems it creates, and
easily miss cases like the above. (I'm certainly not saying that Dave
Abrahams is a sloppy programmer because he missed it. And similar
errors can occur even without exceptions. But personally, I find it
difficult enough to reason about a program logically when the single
entry single exit model of structured programming is used rigorously. I
certainly don't need the additional complexity caused by arbitrary
control flow potentially occuring after any function call.)

I'm also curious about the "automated test suite". I wouldn't normally
have been surprised by it missing this, Dave Abrahams sounded so
confident about it. FWIW: any testing of template classes MUST be done
with a class whose constructors and destructors count themselves
(exceptions or not!), and the count verified to ensure that they match.
If the test did this, and he had also followed the advice of another
poster, in throwing an exception in the first constructor, then in the
second, and so on, until the entire test ran without throwing an
exception, the test suite would have caught the error.

--
James Kanze home: ka...@gabi-soft.fr +33 (0)1 39 55 85 62
office: ka...@vx.cit.alcatel.fr +33 (0)1 69 63 14 54
GABI Software, Sarl., 22 rue Jacques-Lemercier, F-78000 Versailles France
-- Conseils en informatique industrielle --

Dave Abrahams

unread,
Apr 26, 1997, 3:00:00 AM4/26/97
to

In article <5jqako$8...@netlab.cs.rpi.edu>, James Kanze wrote:

> Except that I think that Pop may cause an object to never be destructed,
> which is a memory leak (or worse) in potential.

Ouch! You're right, of course. How embarassing - my only excuse is haste.
After spending weeks implementing, testing, and documenting an
exception-safe STL, I decided this little problem would be easy to solve,
and would help the community. With fire in my belly, I dashed off a
solution - how the error slipped through will become clear shortly...

> Just a nit, but the expression "new char[...]" is not guaranteed to
> return memory sufficiently aligned. Use "::operator new( ... )"
> instead. The probability of this not working is admittedly very low,
> but the explicit call of the operator new function is guaranteed. It
> also has the advantage, IMHO, of indicating that you are allocating raw
> memory, and not an array of characters.

Good points.

>One thing is bothering me: why inheritance rather than membership. Is
> it only to simplify access to the data members, or is there some other
> underlying reason?

No other reason. In fact I would never design classes this way from the
ground up. It mostly arose as an artifact of modifying an existing STL
implementation to support exceptions. I wanted to stay as close to the
original source as possible, to make tracking changes easier. If you have a
class with a few constructors which could fail, this transformation can be
useful in general.

> Concerning exceptions in general: it is interesting that when someone in
> favor of exceptions posts actual code, it isn't exception safe.

I wouldn't say that in general. True, I made a stupid mistake here. Take a
look at the STL implementation and its documentation, which I posted first.
I think you'll find that a bit more rigorous.

> Now, I
> wouldn't say that exception safe code is impossible. The correction in
> the above case is trivial, for example. But most people (certainly
> including myself) are not atuned to the type of problems it creates, and
> easily miss cases like the above.

This is actually a great opportunity. Any time I find a bug in my code, I
ask "what could I have done differently that would have helped me catch the
bug?" Was this bug in fact a problem caused by the use of exceptions? I
don't think so: I was certainly aware that T's copy constructor could fail
when I was writing this bit. In fact, I was focused on the copy constructor
that could fail when the result was returned from the function. When I look
back at what happened, I conclude that this bug was caused by... a blind
reliance on the test suite and A FAILURE TO STEP THROUGH THE CODE. In an
earlier message James Kanze said,

'Of course, you don't "step through" code. What good does that do?'

It reveals all kinds of errors. Using a coverage tool only goes halfway. If
you step through your code, you can see more than just problems with the
instructions - you can watch the data. It is also a second opportunity to
review what you've written - a kind of "self-code-review". See "Writing
Solid Code", By Steve Maguire for more information.

>(I'm certainly not saying that Dave Abrahams is a sloppy programmer
because he missed it.

I really would be in no position to argue in this case.

>And similar errors can occur even without exceptions.

Exactly. In this case, I'm sure I would have made the same mistake without
exceptions.

>But personally, I find it
> difficult enough to reason about a program logically when the single
> entry single exit model of structured programming is used rigorously. I
> certainly don't need the additional complexity caused by arbitrary
> control flow potentially occuring after any function call.)

I certainly can't argue with what you personally find easier. Nonetheless,
I wonder if you've allowed yourself to fully adjust to the exception
paradigm yet. Have you ever tried to write a program using exceptions
exclusively? Control paths become less important, and focus begins to shift
to those places where resources are allocated and program state is
changed.You begin to notice these points and work to insure that their
effects can be undone in the face of subsequent failures

> I'm also curious about the "automated test suite". I wouldn't normally
> have been surprised by it missing this, Dave Abrahams sounded so
> confident about it.

Answers to follow...

> FWIW: any testing of template classes MUST be done
> with a class whose constructors and destructors count themselves
> (exceptions or not!), and the count verified to ensure that they match.
> If the test did this, and he had also followed the advice of another
> poster, in throwing an exception in the first constructor, then in the
> second, and so on, until the entire test ran without throwing an
> exception, the test suite would have caught the error.

Actually, that was MY advice, and I did follow it. What in fact happened
was that I was looking in the wrong place for the output of the test suite.
I just ran my suite again without changing anything, and it finds the error
every time.

So, in the interests of completeness and without any further ado, allow me
to post a correct version of Pop(), which really and truly DOES pass the
test suite.

T Stack<T>::Pop() // if empty, returns default-constructed


T
{
if ( Count() == 0 )
return T();

T result( v_[vused_ - 1] );
--vused_;


destroy( v_ + vused_ );
return result;
}

-Dave

James Kanze

unread,
Apr 29, 1997, 3:00:00 AM4/29/97
to

"Dave Abrahams" <abra...@motu.com> writes:

|> In article <5jqako$8...@netlab.cs.rpi.edu>, James Kanze wrote:

|> >One thing is bothering me: why inheritance rather than membership. Is
|> > it only to simplify access to the data members, or is there some other
|> > underlying reason?
|>
|> No other reason. In fact I would never design classes this way from the
|> ground up. It mostly arose as an artifact of modifying an existing STL
|> implementation to support exceptions. I wanted to stay as close to the
|> original source as possible, to make tracking changes easier. If you have a
|> class with a few constructors which could fail, this transformation can be
|> useful in general.

OK. I was curious, more than anything else. I have no qualms about
using PRIVATE inheritance whenever it makes the implementation more
convenient (whereas I insist that all public inheritance meet the Liskov
substitution principle).

|> > Concerning exceptions in general: it is interesting that when someone in
|> > favor of exceptions posts actual code, it isn't exception safe.
|>
|> I wouldn't say that in general. True, I made a stupid mistake here. Take a
|> look at the STL implementation and its documentation, which I posted first.
|> I think you'll find that a bit more rigorous.

Probably. I think my point still stands. Particularly since, if I look
at my feed, I was the only one who spotted it. I've posted silly errors
like this in non-exception code, and there were generally 10 or more
followups to point out the error in the next couple of hours.

My point is mainly that exceptions are a new technology, and this means
that there is an extra risk factor involved in using them. Whether this
risk factor is offset by other advantages probably depends on the
application, the programming environment, your own habits, your
coworkers habits, and who knows what else. I do think, however, that if
you want to use exceptions, you should be aware of the risk factor.

|> > Now, I
|> > wouldn't say that exception safe code is impossible. The correction in
|> > the above case is trivial, for example. But most people (certainly
|> > including myself) are not atuned to the type of problems it creates, and
|> > easily miss cases like the above.
|>
|> This is actually a great opportunity. Any time I find a bug in my code, I
|> ask "what could I have done differently that would have helped me catch the
|> bug?" Was this bug in fact a problem caused by the use of exceptions? I
|> don't think so: I was certainly aware that T's copy constructor could fail
|> when I was writing this bit. In fact, I was focused on the copy constructor
|> that could fail when the result was returned from the function. When I look
|> back at what happened, I conclude that this bug was caused by... a blind
|> reliance on the test suite and A FAILURE TO STEP THROUGH THE CODE. In an
|> earlier message James Kanze said,
|>
|> 'Of course, you don't "step through" code. What good does that do?'
|>
|> It reveals all kinds of errors. Using a coverage tool only goes halfway. If
|> you step through your code, you can see more than just problems with the
|> instructions - you can watch the data. It is also a second opportunity to
|> review what you've written - a kind of "self-code-review". See "Writing
|> Solid Code", By Steve Maguire for more information.

I'm all in favor of "self-code-review"; in production code when I work
alone, I will always read the code at least a week after having written
it. (Earlier, and my mind is still "fixed" with the patterns it had
when I wrote it, so I don't see the errors.) I'll admit, however, that
I fail to see how watching the data in a debugger would make a
difference here. Anyway, I'm sure that the difference is psychological,
and depends on the programmer, so it really cannot be argued. I'm sure,
for example, you knew that the -- decremented the variable without
seeing it happen in the debugger. I can only suppose that seeing a data
value change somehow "rings a bell" with you that it doesn't with me.

My own system is somewhat different. It is partially based on
mathematic proof, and reasoning about invariants, which is why
exceptions cause so many problems, and partially on looking for things
that have caused trouble for me (or others) in the past. One rule with
regards to exceptions (picked up from other posters here), is to be
doubly suspicious any time an exception can occur after a variable has
been modified; the ideal situation is to verify that everything will
work before changing anything. I find that such rules, mostly derived
from experience, catch an enormous number of errors.

|> >But personally, I find it
|> > difficult enough to reason about a program logically when the single
|> > entry single exit model of structured programming is used rigorously. I
|> > certainly don't need the additional complexity caused by arbitrary
|> > control flow potentially occuring after any function call.)
|>
|> I certainly can't argue with what you personally find easier. Nonetheless,
|> I wonder if you've allowed yourself to fully adjust to the exception
|> paradigm yet.

I'm sure I haven't. Given the time I find it takes to fully adjust to a
new paradigm, I wonder if many people have. This is the risk factor I
spoke about earlier.

|> Have you ever tried to write a program using exceptions
|> exclusively? Control paths become less important, and focus begins to shift
|> to those places where resources are allocated and program state is
|> changed.You begin to notice these points and work to insure that their
|> effects can be undone in the face of subsequent failures

That's an interesting point of view. In a way, you become totally OO;
there is no procedural part that isn't somehow encapsulated in
exceptions (which are also objects).

I don't think that this is the way C++ was designed to be written, but
it sounds like an interesting path in which to experiment.

|> > I'm also curious about the "automated test suite". I wouldn't normally
|> > have been surprised by it missing this, Dave Abrahams sounded so
|> > confident about it.
|>
|> Answers to follow...
|>
|> > FWIW: any testing of template classes MUST be done
|> > with a class whose constructors and destructors count themselves
|> > (exceptions or not!), and the count verified to ensure that they match.
|> > If the test did this, and he had also followed the advice of another
|> > poster, in throwing an exception in the first constructor, then in the
|> > second, and so on, until the entire test ran without throwing an
|> > exception, the test suite would have caught the error.
|>
|> Actually, that was MY advice, and I did follow it. What in fact happened
|> was that I was looking in the wrong place for the output of the test suite.
|> I just ran my suite again without changing anything, and it finds the error
|> every time.

So the way to avoid this error in the future is not to step through the
code, but the set up your environment so that you cannot avoid reading
the error file. (My own test suites always terminate with a message on
the screen: no errors found, or x errors found. Whether or not an error
was found also affects the return code, and the test suites are used in
automated regression tests, which won't install the new version where I
can see it unless the tests all pass. Of course, the result is that
they are complex enough that I don't take the time to use them on code
that I post:-).)

--
James Kanze home: ka...@gabi-soft.fr +33 (0)1 39 55 85 62
office: ka...@vx.cit.alcatel.fr +33 (0)1 69 63 14 54
GABI Software, Sarl., 22 rue Jacques-Lemercier, F-78000 Versailles France
-- Conseils en informatique industrielle --

[ Send an empty e-mail to c++-...@netlab.cs.rpi.edu for info ]

Reply all
Reply to author
Forward
0 new messages