Assertion vs Exception Handling

M

Michael Doubez

"Asserts are *mostly* a debugging tool" is an opinion yes.  Asserts should
only fire during the development phase highlighting bugs that need to be
fixed.  *Most of the time* shipping product with asserts enabled is an
admission that you are either knowingly shipping buggy software or software
which has not been adequately tested.  I say *most of the time* as some
software requires a very high degree defensiveness where data corruption or
bad behaviour due to non-crash causing bugs is unacceptable.

Could you give me an example of a product that will prefer incorrect
behavior over a crash ? Software products usually do deliver value.
 
M

Michael Doubez

Obviously a crash is always preferable over incorrect behaviour and
obviously all software should be adequately tested to ensure neither happens
or the probability of it happening is as low as possible.  Games are an
example of software that needn't have asserts enabled in released software
and this has nothing to do with performance but more to do with the fact
that if a game starts misbehaving it is unlikely to corrupt vital data.

Ok. As I said in a earlier post, you can remove assert when you can
afford it.

And I agree that graceful termination is always better (I imagine the
frustration when loosing 15 hours of gameplay).
If
you backup your documents regularly (as you should) then you could argue
that running a Word Processor built with asserts disabled is acceptable.

Acceptable if it happens rarely. But for the cases it happens, I
expect the user would have preferred the crash (with recovery data).
Remember a hard disk can crash at any time corrupting your data.

And a cosmic particle can switch a bit in memory but it is not a
matter of choice.
 
A

Andrew Poelstra

The primary criterion on whether to use an assert of not in a particular
piece of code should be safety: i.e. preventing undesirable or even
catastrophic behaviour if a program is allowed to continue with invalid
state rather than immediately aborting. Performance is a secondary
criterion. You can use common sense with regard to placing asserts without
affecting performance greatly I agree yes.

"Asserts are *mostly* a debugging tool" is an opinion yes. Asserts should
only fire during the development phase highlighting bugs that need to be
fixed. *Most of the time* shipping product with asserts enabled is an
admission that you are either knowingly shipping buggy software or software
which has not been adequately tested. I say *most of the time* as some
software requires a very high degree defensiveness where data corruption or
bad behaviour due to non-crash causing bugs is unacceptable.

All (almost all?) software is buggy. That's common knowledge, to the
point where releasing any software at all counts as admission that
you're releasing known-to-be-buggy software.

Having asserts in place gives you more information when bugs are
found - in particular, if none of them fire, that gives you all
sorts of invariants you know are true at specific points in the
program, something you wouldn't have if asserts were disabled.

That's my 2 cents.
 
A

Andrew Poelstra

Not true, an invariant might be violated at the same place in a program
where it was previously true depending on the current state (data) of the
program. What you say is only true if the program is run to completion with
the same input each time.

I'm not sure I understand what you mean. If asserts are
enabled, and an assert is /there/, its condition was
true at that point in the program.

Sure, if you assert a pointer to be non-NULL then reassign
it three lines later, the pointer may very well be NULL
despite the assert()'s best efforts, but that doesn't mean
that the assert() didn't provide useful information about
the state of the program.
 
A

Andrew Poelstra

You said "specific points in the program" and specific points in a program
can be hit more than once with different state. Just because the invariant
held in one hit doesn't mean that it will hold in another if there is a bug.

/Leigh

I think we're talking past each other. Every time an assert() is
hit, its condition is evaluated. If the condition is false, the
program terminates. There is no room here for bugs to change the
condition.
 
J

John H.

Michael said:
Could you give me an example of a product that will prefer incorrect
behavior over a crash ?

In some circumstances it may be more useful to think about a *part* of
a product that prefers incorrect behavior over a crash. For instance
if I was writing a web browser whose cursor was to turn from an arrow
to a caret when it was over a place I can enter text. For some reason
the resource for the caret in unavailable. I think an end user would
prefer to be able to continue to run the browser with just the arrow
as the cursor rather than having it crash whenever they move their
cursor over editable text.
 
A

Andrew Poelstra

Nonsense: a bug may only manifest under certain conditions/input.

But given that bad input, either:

1. The assert fires, and you know when the program went wrong
(or when it realized it had gone wrong, anyway).

2. The assert does not fire, and you know that whatever it was
checking, is valid.

In either case you have (possibly very valuable) information about
the problem.
 
A

Andrew Poelstra

I agree with that but that does not tally with what you said earlier ("if
none of them fire, that gives you all
sorts of invariants you know are true at specific points in the program,
something you wouldn't have if asserts were disabled."). It only tells you
that all invariants remained valid for a given set of inputs. An invariant
may break on a subsequent program run with different inputs if there is a
bug present whose behaviour is dependent on a given input.

I sort of see what you're saying. But my original point was that
while the result of assert() is indeed "dependent on a given input,"
the input in question would also be that that has the bug:

User: "I enter my name as O'Hara and it shows up as blank!"
Dev: "Did it say "assertion failed" anywhere?"
User: "No."
Dev: "Ah, there were no NULL pointers or negative lengths passed
where there shouldn't be. That quote is doing /something/,
I bet, but it's not doing either of those."
 
J

James Kanze

On 03/16/10 11:49 AM, James Kanze wrote:
I don't see how you can justify that James, most programmes
today probably run on embedded platforms that don't have
disks!

:). But analog to digital converters are often slower than
disks. And neither the sensor measuring air presure in the
brake line, nor the valves controlling the entry and release of
air in the resevoir are very fast.

(Seriously, of course, it depends. Embedded platforms vary
enormously, and while I can remember more than a few where the
Z80 we were using back then spent most of its time waiting,
there's also at least two where we had to use a few tricks to
make it meet the timing requirements.)
 
J

James Kanze

"James Kanze" <[email protected]> wrote in message

[...]
I think you will find that a lot of C++ programmers do care
about performance,

Far too many do unnecessarily. But as i said, when it is
important, you do what you have to. I've worked on a lot of
hard real time applications, where responding too late to an
event is a fatal error.

Such applications aren't the majority. Even in embedded
systems, today, it's often cheaper to use a more powerful
processor than for programmers to optimize. (But in practice,
politics generally enters into such decisions.)
I do, games programmers certainly do, embedded programmers
probably do especially when the CPU is not all that fast. It
is woolly thinking to release products with optimizations
turned off due to some silly sense of non-existent risk - put
more trust in your compiler. You can have asserts in
optimized code - worst that can happen is that a line number
in the assertion report might be misleading or the the assert
affects performance but common sense should dictate where you
place such assertions (e.g. before a loop rather than in a
loop).

That is, more or less, what I've been saying. You don't have
just two modes: debug and release. You have a large gamut of
possibilities. Including disactivating asserts in the critical
portions, but not elsewhere. Or activating some of the checking
in the standard library, but not all. Or using different levels
of optimization. In most of the places where I've worked
before, performance wasn't an issue: I was working on large
scale servers, reliability was critical, and if it wasn't fast
enough---the program only ran one one or two machines, so you
just upgraded them (from say 32 cores to 256). Today, the
programs run on literally thousands of machines, and some of the
functions take between 20 minutes and an hour to execute. So we
do deliver with maximum optimization (even if this means finding
work-arounds for the compiler bugs, since VC++ doesn't always
call all of the necessary destructors when optimization is
stepped up), asserts disabled, and anything else which can help
speed done. In fact, we're currently moving to three modes,
since that's what makes sense in our environment.

The whole point is that the defaults don't do you a whole lot of
good. None of our builds correspond exactly to what Microsoft
offers as the defaults for "debug" or "release" modes.
 
J

James Kanze

"Leigh Johnston" <[email protected]> wrote in message

[...]
I wrote without thinking: of course the assertion line number
will be correct even with optimizations turned on, I was
thinking of debuggers sometimes not being able to match
assembler to source code lines when optimizations are turned
on.

The biggest problem is that some debuggers won't show the
correct values for local variables when optimizations are turned
on. (Microsoft's debugger seems to be particularly bad in this
respect.)
 
J

James Kanze

James Kanze <[email protected]> wrote:

[...]
I'd agree that it is worthwhile to ship code with asserts.
However, I'd disagree that you should not turn on optimisation
shipping code. Compiler optimisation can make a huge
difference and very importantly, it allow the developers to
write clear, safe and maintainable code and let the compiler
do it's job.

That's the theory. In practice, the risk of hitting a compiler
bug is significantly greater when optimization is turned on. In
theory, the semantics of optimized code are identical to those
of non-optimized code, and compiler optimization should be
nothing but a win. In practice... I recently lost three weeks
tracking down a bug due to the fact that VC++ doesn't always
call destructors in optimized mode.
Turning off compiler optimisation incur the very big risk of
programmers trying to do the copmpiler job and start writing
obfuscated code because they think it will be more efficient.

If the code is insufficiently fast without optimization, then
turning on optimization is generally the first thing you should
do; it's by far cheaper than having programmers spend time
optimizing manually. But until it's proven that the code is
insufficiently fast without optimization, you're better off
avoiding it.
 
I

Ian Collins

If the code is insufficiently fast without optimization, then
turning on optimization is generally the first thing you should
do; it's by far cheaper than having programmers spend time
optimizing manually. But until it's proven that the code is
insufficiently fast without optimization, you're better off
avoiding it.

This may be the case for hosted code, but it generally isn't the case
for embedded applications. Optimisations affect more than speed, they
can also change the size of the generated code. Every embedded project
I've worked on recently used a combination of optimisations for both
speed and code size.

When you are shipping low cost products, moving to a bigger (in memory
terms) or faster processor can break the budget.
 
J

James Kanze

Sounds dubious, I always have optimizations turned on and have
never seen a problem with destructors not being called.

And I have. It wasn't in code I wrote, and my style wouldn't
have triggered the problem, but when I asked about it a
Microsoft newsgroup, it turned out that it is a known error, and
that it is related to the way the compiler is organized, so that
it will not be fixed anytime soon.
Are you sure you are not thinking of RVO which is perfectly
valid?

Curiously enough, it is related to RVO. The compiler suppresses
a destructor because of RVO. Even if you don't return.

MyType f(int max_tries)
{
while (max_tries > 0) {
MyType p( tentative_results )
// The following is actually some fairly
// complicated calculations...
if (max_tries < 2)
return p;
}
throw std::runtime_error( "didn't work" );
}

Instrument the constructors and the destructors of MyType, and
call the function with max_tries at 5, or something like that,
and see what happens. Basically, at maximum optimization, the
compiler "merges" p and the return value (RVO), and suppresses
the destructor of p. Except that when you loop, rather than
returning, the destructor of p isn't called, and it should be.
(In the actual code, MyType was a boost::shared_ptr. Which led
to a memory leak, and in certain cases, a bad_alloc exception.)
If you are sure that there is a VC++ bug relating to
optimizations and dtors not being called have you raised a
defect report on Microsoft connect?

I first raised the question in one of the Microsoft groups.
It's a known bug.
Do the bugs exist in VC9 or VC10?

Yes. When I found it, we immediately checked with all of the
compilers we have available. No problem with Sun CC or g++
(which no doubt have other bugs, but not this one), but a
problem in every version of VC++. According to the people at
Microsoft, it is inherent in the organization of the compiler,
and will not be fixed anytime soon. In the meantime: yet
another argument for SESE:). I'd have written the function:

boost::shared_ptr<MyClass> f(int retries)
{
boost::shared_ptr<MyClass> retval;
while (retval.get() == NULL && retries > 0) {
retval = new MyClass;
// ...
if (! retval->succeeeded())
retval.reset();
-- retries;
}
if (retval.get() == NULL)
throw ...
return retval;
}

which causes no problems (since there is no conditional return),
 
J

James Kanze

This may be the case for hosted code, but it generally isn't
the case for embedded applications. Optimisations affect more
than speed, they can also change the size of the generated
code. Every embedded project I've worked on recently used a
combination of optimisations for both speed and code size.

Agreed. That should read "insufficiently fast or too big".
When you are shipping low cost products, moving to a bigger
(in memory terms) or faster processor can break the budget.

I know. While a lot of my recent applications have only run on
a couple of machines (or only one), some earlier applications
has run one thousands (and in one case, 7 million) machines. On
those, you're willing to invest a lot of development time just
to gain one ROM on the PCB.
 
J

James Kanze

"James Kanze" <[email protected]> wrote in message

[...]
To be taken seriously please post a VC++ testcase which
illustrates this problem (dtor not being called with
optimizations turned on) and post a defect report on Microsoft
Connect if you have not already.

http://connect.microsoft.com/Visual...destructor-calls-when-optimization-is-enabled

I'm not the first to have encountered it.
I always compile with optimizations turned on and have not
suffered from this with VC9 at least.

It depends somewhat on your coding style. I'd never have
encountered in my own code, either, because I rigorously use
SESE. But the code in which I encountered it is perfectly legal
C (and arguably a case where not using SESE is valid).

My example code was:
----- CompilerError.cpp -----
#include <iostream>
#include <stdlib.h>

class Tracker
{
public:
static int instance_count ;
#define TRACK(f) std::cout << "Tracker::" #f << "(), this = " << this
<< std::endl;
Tracker() { TRACK(ctor); ++ instance_count ;}
Tracker(Tracker const& ) { TRACK(copy); ++ instance_count ;}
~Tracker() { TRACK(dtor); -- instance_count; }
Tracker const& operator=(Tracker const& ) { TRACK(asgn); return
*this; }
#undef TRACK

};

int Tracker::instance_count = 0;

Tracker f()
{
for ( int i = 0; i < 2; ++ i )
{
std::cout << i << ": start" << std::endl;
Tracker t;
if ( i != 0 )
{
std::cout << i << ": returning" << std::endl;
return t;
}
std::cout << i << ": end" << std::endl;
}
std::cerr << "I don't believe it" << std::endl;
abort();

}

void g()
{
Tracker t(f());
std::cout << "After f()" << std::endl;

}

int
main()
{
g();
std::cout << Tracker::instance_count << " remaining Trackers" <<
std::endl;
return 0;
}
----- CompilerError.cpp -----

(Copy pasted from my original posting in
microsoft.public.vc.language. I hope the neither Windows nor
Google introduce any anomalies.)
 
J

Joshua Maurice

"James Kanze" <[email protected]> wrote in message

    [...]
To be taken seriously please post a VC++ testcase which
illustrates this problem (dtor not being called with
optimizations turned on) and post a defect report on Microsoft
Connect if you have not already.

http://connect.microsoft.com/VisualStudio/feedback/details/336316/mis...

I'm not the first to have encountered it.
I always compile with optimizations turned on and have not
suffered from this with VC9 at least.

It depends somewhat on your coding style.  I'd never have
encountered in my own code, either, because I rigorously use
SESE.  But the code in which I encountered it is perfectly legal
C (and arguably a case where not using SESE is valid).

My example code was:
----- CompilerError.cpp -----
#include <iostream>
#include <stdlib.h>

class Tracker
{
public:
    static int instance_count ;
#define TRACK(f) std::cout << "Tracker::" #f << "(), this = " << this
<< std::endl;
    Tracker() { TRACK(ctor); ++ instance_count ;}
    Tracker(Tracker const& ) { TRACK(copy); ++ instance_count ;}
    ~Tracker() { TRACK(dtor); -- instance_count; }
    Tracker const& operator=(Tracker const& ) { TRACK(asgn); return
*this; }
#undef TRACK

};

int Tracker::instance_count = 0;

Tracker f()
{
    for ( int i = 0; i < 2; ++ i )
    {
            std::cout << i << ": start" << std::endl;
            Tracker t;
            if ( i != 0 )
            {
                    std::cout << i << ": returning" << std::endl;
                    return t;
            }
            std::cout << i << ": end" << std::endl;
    }
    std::cerr << "I don't believe it" << std::endl;
    abort();

}

void g()
{
    Tracker t(f());
    std::cout << "After f()" << std::endl;

}

int
main()
{
    g();
    std::cout << Tracker::instance_count << " remaining Trackers" <<
std::endl;
    return 0;}

----- CompilerError.cpp -----

(Copy pasted from my original posting in
microsoft.public.vc.language.  I hope the neither Windows nor
Google introduce any anomalies.)

This is amazing. I would have never believed it myself if I didn't
test it first hand. That newer microsoft compilers, or any large scale
commercial C++ compiler could be so broken is almost beyond
comprehension. IMHO, the entire point of C++ (or the most important
point of C++) is the guaranteed implicit invocation of destructors of
stack objects and member sub-objects (and others). If I did not have
this, I might have moved to a different language much earlier for my
own toy work and my language of choice. Destructors are hugely
important to C++, and they are C++'s fundamental underpinnings. This
bug is insane, though perhaps more insane is that Microsoft has not
fixed it yet. I cannot believe that this bug was first hit in the year
2008. I'm sure someone doing toy work running a memory leak detector
(even a simple operator new replacement scheme) would have detected
this. Did they even bother to examine this optimization and how it
would work with anything but their single toy example? Seriously.
There is no excuse besides gross incompetence that this was even
allowed to ship. This is not some obscure use case. This is basic
usage that people do every day.

....

So, how much worse of an optimizing compiler is gcc nowadays for win32
anyway? Might be time to switch.
 
N

Nick Keighley

I think it was "the truth" bit that pressed my button. This is more a
matter of how you look at things. I don't think of assert() as a
debugging tool I think of it as a verification tool. It gives me basic
confidence in a piece ("well whatever is happening it hasn't been
passed a null file object"). And also a last chance error catcher.
Think of it like a fire alarm, you don't expect fires in your building
but if they happen you want them caught as quickly as possible.

Let me rephrase:

The primary criterion on whether to use an assert of not in a particular
piece of code should be safety: i.e. preventing undesirable or even
catastrophic behaviour if a program is allowed to continue with invalid
state rather than immediately aborting.

and as part of your validation strategy

 Performance is a secondary criterion.

depending on your application domain, but generally yes.

 You can use common sense with regard to placing asserts without
affecting performance greatly I agree yes.

"Asserts are *mostly* a debugging tool" is an opinion yes.  Asserts should
only fire during the development phase highlighting bugs that need to be
fixed.  *Most of the time* shipping product with asserts enabled is an
admission that you are either knowingly shipping buggy software or software
which has not been adequately tested.

Or I am admitting that I am not perfect.

 I say *most of the time* as some
software requires a very high degree defensiveness where data corruption or
bad behaviour due to non-crash causing bugs is unacceptable.

I can hardly imagine an application where blundering on after
invarients have been violated is ever good.
 
J

James Kanze

"Yannick Tremblay" <[email protected]> wrote in message

[...]
Indeed and it is because of RAII and exceptions that there is
little difference between SESE and SEME in C++ except one of
coding style and the fact that SESE can sometimes make a
function easier to understand. You could argue that SESE is a
lie in C as well when you consider the existence of
setjmp/longjmp. :)

You could argue that SESE is a lie in any language, when you
consider the existance of machine crashes:). SESE only really
concerns analysis of program correction and understanding of the
code. If you're functions are complex enough that the
alternative control flows aren't easily analysable, they're
probably too long, SESE or not. SESE simplifies analysis, but
it doesn't guarantee correctness.

(And exceptions do introduce additional issues. But a function
which exits via an exception generally doesn't have to meet its
full set of post-conditions.)
 
I

Ian Collins

[...]
The correct alternative (which I am sure James does) is to
write exception safe code that can cope with exceptions and
stack unrolling at any line in the function. Such code gain
a lot less value from SESE but still suffer by the (slight)
obfuscation.
Indeed and it is because of RAII and exceptions that there is
little difference between SESE and SEME in C++ except one of
coding style and the fact that SESE can sometimes make a
function easier to understand. You could argue that SESE is a
lie in C as well when you consider the existence of
setjmp/longjmp. :)

You could argue that SESE is a lie in any language, when you
consider the existance of machine crashes:). SESE only really
concerns analysis of program correction and understanding of the
code. If you're functions are complex enough that the
alternative control flows aren't easily analysable, they're
probably too long, SESE or not. SESE simplifies analysis, but
it doesn't guarantee correctness.

(And exceptions do introduce additional issues. But a function
which exits via an exception generally doesn't have to meet its
full set of post-conditions.)

But it probably should clean up before leaving. Which leads to
exception safe code and relegates the SESE/SEME decision to one of style.
 

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,774
Messages
2,569,599
Members
45,173
Latest member
GeraldReund
Top