Re: [GD-General] Assertions, pre/post-conditions and call tracing
Brought to you by:
vexxed72
|
From: Jesse J. <jes...@mi...> - 2002-01-15 22:16:08
|
At 8:19 AM -0800 1/15/02, Brian Hook wrote:
>I've gotten into the habit of making tons of function pre-conditions
>and post-conditions that can optionally be compiled away. This is a
>Eiffel habit picked up second-hand from a former coworker of mine.
>
>In Eiffel, you can set up a bunch of safeguards to do basic sanity
>checking. The safeguards are pre-conditions (validate state and
>parameters before a method/"feature" is executed); post-conditions
>(validate state and parameters after a "feature" is executed); and
>class-invariants (check the integrity of the object after ANY
>"feature" is executed).
>
>Eiffel does this with "require" and "ensure" clauses that are
>analogous to assert() expressions in C++, but are a bit more
>readable. In addition, the "ensure" clause is guaranteed to be
>evaluated no matter how the function is exited (so you don't have to
>do a "goto ensure" all over your code to make sure you exit out the
>same place).
Are you sure this is true? It doesn't make sense to me: if a method
is unable to complete and has to throw an exception the
post-condition need not hold (eg an AddObject() method may not
actually add the object if it runs out of memory).
>I don't believe there's any clean way to do a class-invariant in
>C++, but I may be wrong on that count. The closest thing I can
>think of is making each class declare an InvarianceChecker class
>that is instanced at the beginning of each method and whose
>constructor/destructor pair do verification.
Invariants aren't too bad. The hard things revolve around pre and
post-conditions. The hardest is that derived classes are allowed to
weaken pre-conditions and strengthen post-conditions (ie they may
accept a wider range of input and more tightly specify what they
return). I don't think there is a good way to handle this in C++
barring a custom preprocessor. However, personally, I haven't missed
it much.
The other problem is that objects can temporarily fall into a bad
state as their methods execute. This can cause problems if a public
method winds up being called. The fix is to only call the invariant
method when a public method is first entered and exited. This does
turn out to be a problem in my experience.
I used to handle handle the nesting issues via a mixin class, but
it's a bit ugly to muck with inheritance for something like this.
What I do now is use a static map that maps this pointers to nesting
counts.
FWIW here's the code I use:
#if DEBUG
void AssertFailed(const char* expr, const char* file, int line);
#define ASSERT(p) (!(p) ? AssertFailed(#p,
__FILE__, __LINE__) : (void) 0)
#define PRECONDITION(p) ASSERT(p)
#define POSTCONDITION(p) ASSERT(p)
#define OBSERVE(type, name, var) type name(var)
#else
inline void DUMMY_TRACE(...) {}
#if ASSERTS_THROW
void AssertFailed(const char*, const char*, int);
#define ASSERT(p) (!(p) ? AssertFailed(#p,
__FILE__, __LINE__) : (void) 0)
#define PRECONDITION(p) ASSERT(p)
#define POSTCONDITION(p) ASSERT(p)
#define OBSERVE(type, name, var) type name(var)
#else
#define ASSERT(p) ((void) 0)
#define PRECONDITION(p) ((void) 0)
#define POSTCONDITION(p) ((void) 0)
#define OBSERVE(type, name, var) ((void) 0)
#endif
#endif
AssertFailed either breaks into the debugger or throws an exception.
Here's an example of how the macros are used:
void Names::AddName(const std::string& name)
{
PRECONDITION(!name.empty());
PRECONDITION(!this->HasName(name));
OBSERVE(uint32, oldCount, mNames.size());
CHECK_INVARIANT;
(void) mNames.insert(name);
POSTCONDITION(oldCount+1 == mNames.size());
POSTCONDITION(this->HasName(name));
}
Note that invariants are called explicitly via a macro. I used to
call these from the PRECONDITION macro, but that's annoying because
most classes don't have invariants and PRECONDITION and POSTCONDITION
are useful pretty much everywhere (if nothing else as a documentation
aid).
Here's the (core) code I use for invariants. The CHECK_INVARIANT
creates a stack based class that calls the invariant method when it's
constructed and when it's destroyed:
#if DEBUG
#if __GNUC__ // $$ gcc 2.95 crashes when this is a member function...
template <class T>
void InvokeInvariant(const void* object)
{void (T::*method)() const = &T::Invariant; (static_cast<const
T*>(object)->*method)();}
#endif
class CheckInvariant {
public:
~CheckInvariant()
{LeavingObject(mObject); if (mInvoker) mInvoker(mObject);}
template <class T>
CheckInvariant(const T* object) : mObject(object)
{mInvoker = NULL; if (EnteringObject(mObject))
{mInvoker = &InvokeInvariant<T>; mInvoker(mObject);}}
public:
static bool EnteringObject(const void* object);
static void LeavingObject(const void* object);
private:
#if !__GNUC__
template <class T>
static void InvokeInvariant(const void* object)
{void (T::*method)() const = &T::Invariant;
(static_cast<const T*>(object)->*method)();}
#endif
private:
void (*mInvoker)(const void*);
const void* mObject;
};
#endif
#if DEBUG
#define CHECK_INVARIANT CheckInvariant _checker(this)
#define CALL_INVARIANT this->Invariant()
#else
#define CHECK_INVARIANT ((void) 0)
#define CALL_INVARIANT ((void) 0)
#endif
static std::map<const void*, int32> sNesting;
#if THREADED
static boost::mutex sMutex;
#endif
bool CheckInvariant::EnteringObject(const void* thisPtr)
{
ValidatePtr(thisPtr);
#if THREADED
boost::mutex::scoped_lock lock(sMutex);
#endif
int32& count = sNesting[thisPtr]; // default construction of a
POD type zero-initializes (see section 8.5 of the standard)
bool entered = ++count == 1;
return entered;
}
void CheckInvariant::LeavingObject(const void* thisPtr)
{
ValidatePtr(thisPtr);
#if THREADED
boost::mutex::scoped_lock lock(sMutex);
#endif
std::map<const void*, int32>::iterator iter = sNesting.find(thisPtr);
ASSERT(iter != sNesting.end());
ASSERT(iter->second > 0);
if (--(iter->second) == 0)
sNesting.erase(iter);
}
Here's an example of an Invariant:
void PerspectiveCamera::Invariant() const
{
ASSERT(mHither > 0.0);
ASSERT(mHither < mYon);
ASSERT(IsUnitVector(mUpVector));
ASSERT(IsUnitVector(mViewVector));
ASSERT(AreOrthognal(mUpVector, mViewVector));
ASSERT(mFieldOfView > 0.0);
ASSERT(mFieldOfView <= 180.0);
ASSERT(mAspectRatio > 0.0);
}
I've seen some published DbC code that had the Invariant return a
bool that was then asserted on, but IMO this is completely
wrong-headed: it makes it way too hard to figure out exactly what
failed.
-- Jesse
|