[Home]
[Search]
[D]
It makes sense to review what DbC is, how it is done in D, and stack that up with what each of the various C++ DbC techniques can do.
Digital Mars C++ adds extensions to C++ to support DbC, but they are not covered here because they are not part of standard C++ and are not supported by any other C++ compiler.
assert does not know anything about class invariants, and does not throw an exception when it fails. It just aborts the program after writing a message. assert relies on a macro text preprocessor to work.
assert is where explicit support for DbC in Standard C++ begins and ends.
class A
{
invariant() { ...contracts... }
this() { ... } // constructor
~this() { ... } // destructor
void foo() { ... } // public member function
}
class B : A
{
invariant() { ...contracts... }
...
}
To accomplish the equivalent in C++ (thanks to Bob Bell for providing
this):
templateThere's an additional complication with A::foo(). Upon every normal exit from the function, the invariant() should be called. This means that code that looks like:inline void check_invariant(T& iX) { #ifdef DBC iX.invariant(); #endif } // A.h: class A { public: #ifdef DBG virtual void invariant() { ...contracts... } #endif void foo(); }; // A.cpp: void A::foo() { check_invariant(*this); ... check_invariant(*this); } // B.h: #include "A.h" class B : public A { public: #ifdef DBG virtual void invariant() { ...contracts... A::invariant(); } #endif void bar(); }; // B.cpp: void B::barG() { check_invariant(*this); ... check_invariant(*this); }
int A::foo()
{
...
if (...)
return bar();
return 3;
}
would need to be written as:
int A::foo()
{
int result;
check_invariant(*this);
...
if (...)
{
result = bar();
check_invariant(*this);
return result;
}
check_invariant(*this);
return 3;
}
Or recode the function so it has a single exit point.
One possibility to mitigate this is to use RAII techniques:
int A::foo()
{
#if DBC
struct Sentry {
Sentry(A& iA) : mA(iA) { check_invariants(iA); }
~Sentry() { check_invariants(mA); }
A& mA;
} sentry(*this);
#endif
...
if (...)
return bar();
return 3;
}
The #if DBC is still there because some compilers may not
optimize the whole thing away if check_invariants compiles to nothing.
void foo()
in { ...preconditions... }
out { ...postconditions... }
body
{
...implementation...
}
This is nicely handled in C++ with the nested Sentry struct:
void foo()
{
struct Sentry
{ Sentry() { ...preconditions... }
~Sentry() { ...postconditions... }
} sentry;
...implementation...
}
If the preconditions and postconditions consist of nothing
more than assert macros, the whole doesn't need to
be wrapped in a #ifdef pair, since a good C++ compiler will
optimize the whole thing away if the asserts are turned off.
But suppose foo() sorts an array, and the postcondition needs to walk the array and verify that it really is sorted. Now the shebang needs to be wrapped in #ifdef:
void foo()
{
#ifdef DBC
struct Sentry
{ Sentry() { ...preconditions... }
~Sentry() { ...postconditions... }
} sentry;
#endif
...implementation...
}
(One can make use of the C++ rule that templates are only
instantiated when used can be used to avoid the #ifdef, by
putting the conditions into a template function referenced
by the assert.)
Let's add a return value to foo() that needs to be checked in the postconditions. In D:
int foo()
in { ...preconditions... }
out (result) { ...postconditions... }
body
{
...implementation...
if (...)
return bar();
return 3;
}
In C++:
int foo()
{
#ifdef DBC
struct Sentry
{ int result;
Sentry() { ...preconditions... }
~Sentry() { ...postconditions... }
} sentry;
#endif
...implementation...
if (...)
{ int i = bar();
#ifdef DBC
sentry.result = i;
#endif
return i;
}
#ifdef DBC
sentry.result = 3;
#endif
return 3;
}
Now add a couple parameters to foo(). In D:
int foo(int a, int b)
in { ...preconditions... }
out (result) { ...postconditions... }
body
{
...implementation...
if (...)
return bar();
return 3;
}
In C++:
int foo(int a, int b)
{
#ifdef DBC
struct Sentry
{ int a, b;
int result;
Sentry(int a, int b)
{ this->a = a;
this->b = b;
...preconditions...
}
~Sentry() { ...postconditions... }
} sentry(a,b);
#endif
...implementation...
if (...)
{ int i = bar();
#ifdef DBC
sentry.result = i;
#endif
return i;
}
#ifdef DBC
sentry.result = 3;
#endif
return 3;
}
class A
{
void foo()
in { ...Apreconditions... }
out { ...Apostconditions... }
body
{
...implementation...
}
}
class B : A
{
void foo()
in { ...Bpreconditions... }
out { ...Bpostconditions... }
body
{
...implementation...
}
}
The semantics for a call to B.foo() are:
class A
{
protected:
#if DBC
int foo_preconditions() { ...Apreconditions... }
void foo_postconditions() { ...Apostconditions... }
#else
int foo_preconditions() { return 1; }
void foo_postconditions() { }
#endif
void foo_internal()
{
...implementation...
}
public:
virtual void foo()
{
foo_preconditions();
foo_internal();
foo_postconditions();
}
};
class B : A
{
protected:
#if DBC
int foo_preconditions() { ...Bpreconditions... }
void foo_postconditions() { ...Bpostconditions... }
#else
int foo_preconditions() { return 1; }
void foo_postconditions() { }
#endif
void foo_internal()
{
...implementation...
}
public:
virtual void foo()
{
assert(foo_preconditions() || A::foo_preconditions());
foo_internal();
A::foo_postconditions();
foo_postconditions();
}
};
Something interesting has happened here. The preconditions can
no longer be done using assert, since the results need
to be OR'd together. I'll leave as a reader exercise adding
in a class invariant, function return values for foo(),
and parameters
for foo().
By adding support for DbC into the language, D offers an easy way to use DbC and get it right. Being in the language standardizes the way it will be used from project to project.
Chapters 24.3.7.1 to 24.3.7.3 discuss Contract Programming in C++ in
The C++ Programming Language Special Edition
Bjarne Stroustrup, Addison-Wesley