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).
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.
- Specify invalid values for input (example: if you divide by something, better put in that it may not be zero).
- Specify any valid ranges for input (example: if your function will only work as expected for values 0, 1, and 2, specify that).
- When dealing with templates, specify specific operations and operators that must be defined on the templated type(s) for the function to function.
- Be descriptive about operators. "
T
must have-
", OK, sure, but which-
? Unary, binary? What does it have to be defined for?T - U
,T - double
?- When templated on several types, describe how those must be able to interact; e.g.,
T * U
must be defined, orFunc(T)
.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:
- "<parameter> must be a valid <type>". WAY too ambiguous; what is valid? If any values are allowed, are you saying that what you pass in must be of type <type>? That's the compiler's job to check that for you. Don't do this.
- "<parameter> must be fully constructed." If you manage to get into the function without fully constructed objects, your computer probably does not work like it should. Objects are inherently constructed; if not, the program enters an error state (or should).
- "<parameter> must have values". It is the developer's responsibility to make sure the data they pass to your function is what they want it to be. Unless a certain value will cause your function to break, you may assume GIGO (garbage in, garbage out).
- "user must pass menu option to function". Just...no. If it's not clear what your parameter is supposed to be, you should probably rename it to make sense. Also, what happens if the user wants to use that function for a non-menu thing?
- "<parameter> must be a double, float, or integer type". If you do this (and your code is a template), what's the point of a template? Overload it or template specialize it.
- "Class <class you are currently implementing> must exist". You just implemented it. It exists. That's a given.
- "The template parameter for the function must match the template parameter for the class". If you use the same template parameter name for both (e.g.,
T
), then the compiler enforces this for you. If you did something fancy with a second template parameter, why bother with that if you're going to put this in the preconditions?
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 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; }
When writing documentation, there are two overarching rules to keep in mind:
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.