CSCI-230 Spring 2000
Data Structures and Algorithms
Project Guidelines

Introduction

This document explains what is expected of you on the projects. These guidelines are intended to help you produce a correct project submission as well as develop good coding habits.

Pay special attention to text that is boxed, as in the following guideline.

Failure to follow these guidelines will severely impact your grade, regardless of how well your submission runs.

To see an example of a simple project that follows these guidelines, look at the files in here.

Requirements for container classes

When writing container classes, you must be sure to include the following member functions:

In some cases one or more of these may not make sense for the class, or the default version may be acceptable. You must provide a comment to this effect when omitting any of the required member functions.

Be sure to implement all required member functions for container classes.

Requirements for iterator classes

When writing iterator classes, you must be sure to include the following member functions:

Some iterators may not support all of the above (for example, foward iterators would not support the decrement or random access operations).

Be sure to implement all required functions for iterator classes.

Miscellaneous coding rules

Containers and functions should, whenever possible, be generic. To do this they must be implemented as template classes and functions.

Use templates to make algorithms and data structures generic.

Often it is useful to have constructors that take arguments; for example, to provide the size of a container:

template <class T>
class container {
 ...
  container(int sz) { ... }  
};
However, this constructor can also act as a conversion between int values and containers. Obviously such a conversion does not make sense, but the following code will compile rather than give an error:
container c;
c = 1;       // Convert integer to a container?
The "explicit" keyword avoids this problem.

Use the keyword explicit with any constructors that are not meant to provide conversions.

Any function argument that is not meant to be modified should be declared const. Any member function that does not modify its object should be declared const. Any function that returns a temporary should return const.

Be const correct.

The standard library (aka the STL) provides a large number of generic data structures and algorithms. Using them will make your coding easier, more readable, and more efficient.

Make use of standard library components whenever possible.

A primary theme in the standard library (and in this course) is the concept of iterators, which work like plain old pointers. Iterators are the key to using the generic algorithms in the standard library effectively.

Use iterators (pointers) in preference to indexing.

Note that if you have already implemented the member function operator==, then it is trivial to write:

template <class T>
class container {
 ...
  bool operator!=(const container & c) const { return !operator==(c); }
};

Likewise, if you already have implemented pre-increment, you can write:

const T operator++(int)()
{
  T old(*this);
  ++*this;
  return old;
}

Always implement operator!= in terms of operator== and post-increment in terms of pre-increment.

The standard library is now contained in a namespace called std. To use the name of a standard component, for example cout, you must do either one or the other of the following:

If you only need to specify a component once or a few times, the first option is probably best. Use the second option for names that you use a lot, like cout, cin, and endl.

You may be tempted to put using namespace std; at the top of all your files. Do not do this!

Never type "using namespace std;".

The return type of the main function is always int (even though some compilers will allow other types). Main either takes no parameters, or the two parameters argc and argv following the C convention. You main function should always look like one or the other of the following:

int main() 
{ 
  ... 
}
or
int main(int argc, char * argv[])
{
  ...
}

Always declare main correctly.

Coding style and comments

Coding style is largely a matter of personal taste. However, it must be readable and consistent. This means that on team projects every member of the team must use the same style.

Use whitespace and indentation consistently to produce readable code.

Comments should help you to understand the code. They are especially useful when you return to code after a period of time and are no longer familiar with it, or when someone other than the original author is looking at the code.

When writing comments, write what you would say if you were explaining the code to someone else. Do not write obvious comments like the following:

// Increment the variable 'x'.
++x;

Instead, try to provide insight as to what the code is intended to do at a higher level:

// Find the in-order successor 's' of the node 'x'.
assert(x->right_child);
node * s = right_child;
while (s->left_child)
  s = s->left_child;

Provide appropriate comments in your code.

In certain situations comments are always required:

Always provide the following required information in comments:

  • Every file must have a comment at the top giving the name of the file, the author's name and email address, and the contents of the file.

  • Every container class must have a comment describing the implementation of the data structure.

  • Every template class or function must have a comment describing the purpose of its template parameters.

  • Every template class or function must have a comment describing what operations must be supported by template parameters. Use common concept names (e.g., Random Access Iterator) where applicable.

  • Every non-trivial function must have a comment noting its running time (and space if appropriate).

  • Every public member function of a class must have a comment describing its purpose and the meaning of any parameters and/or return value.

An exception to the last point above are constructors, destructors, copy constructors, assignment operators, and any other required functions such as size, empty, swap, etc.

It is recommended that you name your C++ source files using the extension ".cxx" and your include files using the extension ".h".

One piece of advice: although it is good practice to separate interface and implementation into .h and .cxx files, this can cause problems with some compilers. For this course, make put both into a single .h file.

Use of assert

A special type of comment is an assertion. Assertions not only document something about the code, they can be checked at run time and will stop the program if an assumption turns out to be wrong. Certain aspects of your code should always be documented with assertions.

Every data structure has an invariant, a collection of properties that are true about it. (For example, the invariant of a binary tree is that the key of each node is greater than the key of every node in its left subtree, and less than the key of every node in its right subtree.)

Every container class must have a private const member function called invariant, taking no arguments and returning type bool, that specifies the invariant for the data structure.

Properties that must be true should be specified using the assert macro. (To use this macro in your code you must include the header <cassert>). For example, to specify the property that a must be less than b we would write:

assert(a < b);

Such an assertion will produce a message at run time if the property does not hold. This almost certainly indicates a bug in your program.

Sometimes a property is so complicated that it would be difficult or impossible to write a formal assertion for it. We will still document such cases by writing an assertion that contains a short string literal describing the property. For example, below we could have saved the original size of the container in a local variable for later comparison, but this would be inefficient and confusing. Instead we write:

template <class T>
class container {
 ...

  void insert(T x) {
    ...
    assert("size of container increased by one");
  }

This trick works because the string literal has a non-zero (pointer) value, which is converted to true in C++.

For algorithms, it is often useful to talk about pre-conditions and post-conditions, properties that are true before and (respectively) after the algorithm is performed. As with the invariant, these can be documented and checked at run time by placing appropriate assertions at the begining and end of the function implementing the algorithm.

State pre- and post-conditions for algorithms using assertions.

Often the algorithm we are interested in performs an operation on a data structure. In this case the invariant is part of both the pre- and post-condition. Since the invariant returns true or false, it can be checked using an assertion like any other property.

Every public member function of a container class must check the invariant as its first and last steps.

There are several exceptions to the above rule. For constructors, it only makes sense to provide a post-condition; likewise, destructors should only provide a pre-condition. The assignment operator and copy constructor (as well as any other public member function taking a member of the class as an argument) should check the invariant for the argument, too. Member functions that are declared const need not check the invariant in the post-condition, since the value of the object will not have changed since it was checked in the pre-condition.

You may find it useful (although it is not required) to add pre- and post-condition assertions to private member functions as well.

One note of caution: if you are not careful you can end up with infinite recursion. Consider the following example.

template <class T>
class container {
public:
  int size() const {
    assert(invariant());
    // Check other pre-conditions and compute and return size...
  }
private:
  bool invariant() const {
    assert(size() >= 0);  // Container cannot have negative size.
  }
};

Be careful not to cause infinite recursion in your assertions!

Error checking

Conditions that depend on the external code using a data structure or algorithm should not be handled as pre-conditions (which are for detecting bugs in the internal code implementing the data structure or algorithm). Rather, these conditions should be explicitly checked, and if they do not hold, an exception should be thrown.

For example, before removing an item from a container, obviously it is a requirement that the container not be empty. This is an external requirement, so we would not write:

assert(!empty());   // Wrong!

Instead, we write:

if (!empty())
  throw container_empty();  // Correct!

The call following the throw statement invokes the constructor of an exception class that must be declared. Exception classes are simply empty classes with meaningful names.

class container_empty {};  // Empty class.

Check for approriate error conditions and throw exceptions.

If the error occurs while your program is running, it will terminate cleanly. Exceptions can also be caught to allow the program to handle the error itself.

Test cases

Your code is going to contain bugs. The best way to find bugs (or to have confidence that your code is working correctly) is to run test cases. A test case is a short program that tests a particular piece of functionality. It might test whether a data structure keeps items in the correct order, or it might check that the appropriate exception is thrown by purposefully causing an error.

A test case should execute within a try-block, and any exceptions that are thrown should be reported as a failure (except, of course, if the test is checking for a particular exception to be thrown).

In addition to printing an appropriate message indicating if the test case passed or failed (and if it failed, why), the main function of a test case should return zero if the test passed and one if it failed. This allows the makefile to automatically flag failed tests.

See the sample files for examples of test cases.

Always write (and run) test cases.

Certain numbers and types of test cases will be required for every project.

A good way to build up a collection of test cases is to write one whenever you encounter a bug. Write a test case that exposes the bug in your code, use the test case code to fix the bug, and then rely on the test case to check that the bug does not return.

Another way to generate useful test cases is to think about the "boundary conditions". For example, does a container work correctly when it is empty? When it is full? Does an iterator work correctly when it is at the begining and end of its container?

Makefile

A makefile is a necessity to manage medium to large programs. In addition, a properly configured makefile makes it easy to run your test cases.

Always provide a makefile for your project.

Use the sample makefile as a guide, and learn how to construct your own.


valoisj@cs.rpi.edu 2000-01-11
http://www.cs.rpi.edu/classes/spring00/dsa/coding.html