Home CS 5201

CS 5201: Pre- and Post-conditions

As part of this course you are expected to produce high-quality documentation for the code you write. This documentation must include pre- and post-conditions for all member and friend functions. Part of the problem with documentation, like English grammar, is that when you are taught it, you don't know enough to appreciate how important it is. This page explains the expectations for your documentation; they are liberally borrowed from the requirements of previous CS 5201 graders (Billy Rhoades and Nate Eloe).

Preconditions

Q) What is a precondition?

A) A precondition is a statement about what must be true about the inputs to a function to guarantee that the function will behave as expected (which you specify in postconditions).

Q) What should a good precondition do?

A) A good precondition should do the following (of course, non-exhaustive list):

In other words, you should specify values that will or will not work for that function. By 'work' here we mean that the function will do something reasonable and not throw an exception. (This does not mean that you can just write "don't pass 0" in your precondition and then not bother throwing an exception should the user pass 0!) Mention here or somewhere in the documentation that your function throws an exception if it does.

If you have a precondition that holds for all or most member functions in a class, it is OK to list that in a precondition for the class, rather than in each member function. However, if, for instance, you have one function that needs sqrt(T) defined, just put that in the pre for that function.

Q) What should a good precondition not do?

A) There are lots of things that we would consider silly preconditions, including the following:

Q) Can a function have no precondition?

A) YES! However, as soon as you introduce templates into your code, if your function does anything at all, you will probably have a precondition. Take the time to carefully analyze what your function does and ask yourself "what will break this?"

So, let's do an example:

 1 /// Compares the magnitude (square root of the sum of the squares
 2 ///       of the values) of the two vectors
 3 /// \pre  ...see below
 4 /// \post None
 5 ///
 6 /// ...bet you thought I made a mistake...
 7 /// \return -1 if |vec1| > |vec2|, 0 if |vec1| == |vec2|, 1 if |vec1| > |vec2|
 8 /// \throws std::domain_error if vec1.size() != vec2.size()
 9 
10 template <class T>
11 int compare_magnitude(const std::vector<T> vec1, const std::vector<T> vec2)
12 {
13   T sum1, sum2;
14   T mag1, mag2;
15   int retval = 0;
16 
17   if(vec1.size() != vec2.size())
18     throw std::domain_error("Vectors are not equal sizes");
19 
20   for (int i=0; i<vec1.size(); i++)
21   {
22     sum1 += vec1[i] * vec1[i];
23     sum2 += vec2[i] * vec2[i];
24   }
25 
26   mag1 = std::sqrt(sum1);
27   mag2 = std::sqrt(sum2);
28 
29   if (mag1 > mag2)
30     retval = -1;
31   else if (mag1 < mag2)
32     retval = 1;
33 
34   return retval;
35 }

First, some good preconditions:

1 /// \pre T * T (multiplication) defined, and results in type T (or is implicitly castable to T)
2 ///      T += T (in place addition) defined
2 ///      T > T and T < T (comparison) defined
3 ///      std::sqrt(T) defined
4 ///      vec1.size() == vec2.size()

The last two deserve some explanation. There is nothing stopping someone from overloading the std::sqrt function to handle a custom class (say, complex numbers or bignums). Also, our function would technically not crash as long as vec1.size() < vec2.size(). However, we have specified that the function will compare the magnitudes; if the sizes are different, we would not be computing the magnitude of vector2. Note that despite including this in the preconditions, we still check and throw an exception should the condition not hold.

Now, some bad preconditions:

1 // T is a numeric type

Not necessarily. Let's not judge programmers who want to exploit the behavior of your function for their own nefarious ends. As long as that functionality is defined, who are we to stop them?

2 // vec1 and vec2 contain valid data

Again, who are you to judge what the valid data is? If you do have specific ideas of invalid data, say that explicitly!

3 // the vector class is included

If you haven't included it, why/how are you using it? If your code requires something to be included, then just include it. If the user's code requires something to be included, that's their problem, not yours.

4 // vec1 is a valid vector<T> 

Just let the compiler do its job!

Postconditions

Postconditions should:

To clarify: I do consider the return documentation to be part of the postcondition. However, in general, if you have a non-const member function, there ought to be something in the postcondition in addition to the return documentation. (The exception to this rule is for animals like T& operator[](const int idx) where the function is non-const because the return type allows the member variables to be modified elsewhere.)

Post conditions should not:

For example, here is a good pre/post block:

1 /// Scalar matrix multiplication. Returns a copy of the result matrix.
2 ///
3 /// \pre  T::operator*=(T) must be defined.
4 /// \post a copy of this matrix with each element multiplied by rhs is returned.
5 DerivedMatrix<T> operator*(const T rhs) const;

Notice the explicit mention of the return method (copy) and what the return value is.

For completeness, I have included the only function that has a postcondition of none. Please note that the below code is © 2017-2018 Natasha Jarus and may not be used without express written permission.

1 void do_nothing() { return; }

Conclusions

When writing documentation, there are two overarching rules to keep in mind:

  1. Don't include detail that the compiler checks at compile time
  2. Write documentation as if the reader will never see your implementation code

I'm hoping you take this information to heart. My job and life is a lot easier (and happier) when you guys write good function docs. I want you folks to succeed, and that includes writing code that if it were to be used by someone else, wouldn't arbitrarily delete data without documenting it somewhere *cough*mongodb driver*cough*. I know it's a pain to write good function docs, but they will save anyone using your library tons of time down the road.