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 |