CSE 332 Programming Guidelines

These guidelines are intended to help you learn and practice good software development habits, which in the long term can help you write more reliable and readable programs and function more effectively as part of a software development team. In the short term, developing these habits also can help you avoid some of the more common mistakes that have resulted in grading deductions in previous semesters.

A. Program Compilation, Execution, and Submission

    Problems with program compilation and execution will often result in major (5-10 point) deductions during grading, though for less severe concerns (for example a compiler warning that should have been fixed but does not indicate a deeper problem with the program, or forgetting to add a usage message for a program) minor (1-3 point) deductions are possible.

  1. Read each lab assignment carefully and completely, and make sure you have done everything the assignment asks you to do. A common reason for deductions on a programming assignment is failure to complete one or more tasks requested for the assignment.

  2. Make sure you submit all the files needed to compile and run your program. Deductions will be made for files that are missing from the submitted code, and the deductions will increase in cost with the number of occasions on which submissions are missing files. A good way to make sure all the files are submitted is to check several places: (1) are they all listed in the Makefile? (2) Did you run make test_turnin and if so did all the files end up in the TEST_TURNIN directory when you did that?

  3. Always submit a "readme" file that documents what the submitted program does, any places where your solution diverged from what was requested in the assignemnt, and the design decisions you made in developing your program. Please ask any remaining questions you may have, which were not answered on the newsgroup or in lab, in your readme file, and we welcome feedback on your impressions of the lab (what was easy, what was difficult, suggestions, etc.).

  4. Test your program thoroughly. Don't just run it a few times and assume it will work correctly when we grade it (using an arbitrary set of tests we design). Instead, think about all the ways your program might fail, and then design additional tests to explore those cases. Think about all the different kinds of inputs a user might provide: could certain inputs (or failure by the user to provide those inputs) result in a divide-by-zero error, an out-of-range index to an array, a stray pointer, or other situations that are likely to lead to a program crash? Similarly, will all allocated memory be released under all possible error conditions, as well as along the "happy path" of program execution?

  5. Always compile with the -Wall switch to see all compiler warnings, and then fix all compiler warnings before submitting your program for grading. If a warning cannot be fixed, and does not indicate a problem in your code, you may use a compiler pragma to silence that specific warning, but then please document that you have done so, and why it was necessary, in your readme file and in your code.

  6. Your program's main function must return 0 on successful termination, and a non-zero value otherwise. If a number of different return values are provided, it's good to document what the different return values mean both in the program source code itself and in the usage message the program generates.

  7. Don't let a thrown exception propagate out of the main function uncaught. Instead, always catch any exceptions that propagate up to the main function and then return an appropriate non-zero code to indicate program failure.

  8. Only use exceptions to unwind the program call stack under abnormal conditions: do not use exceptions as a way to control program execution under normal operating conditions.

  9. Avoid using the same error message for different failures (this often results from "cut and paste" coding). Make sure to document in your readme file all the error messages your program can generate, and the conditions under which each one can arise.

  10. After calling a function that can fail (for example, opening a file), always check for success or failure and then take appropriate action if the function failed. On a few rare occasions there really may be nothing useful to do if a function fails, in which case it is only acceptable not to check the return value if you add a comment explaining why there is nothing useful to do if the return value indicated failure instead of success.

  11. Avoid unnecessary format restrictions, e.g., in general your programs should tolerate blank lines and extra whitespace in input files unless there is an overriding reason not to do so.

  12. Don't dereference a pointer whose value is zero or that points outside the memory locations allocated to your program. If you do so your program will end. Abruptly. This is called an access violation.

  13. Be very careful with pointer arithmetic, and ensure that any non-zero pointer points to some object: if it is not a handle to an object, then it is probably a "handle to an access violation" if it's dereferenced (if you're lucky: see next guideline).

  14. Watch out for the "worst case scenario" with pointer arithmetic mistakes. If a pointer ends up pointing to memory your program owns, but not where you intended it to point, it is possible to corrupt other parts of your program arbitrarily simply by writing to the memory location where the "stray pointer" points. Worms and viruses exploit this weakness of C, C++, and other similarly "unsafe" languages by trying to cause programs to overwrite their program call stacks or other crucial areas, in order to give the worm or virus control over the computer. If your program seems to be corrupted (this often can be seen very well in a debugger), it's definitely time to go back and check your pointer math for statements that have executed up to the point when things got corrupted.

  15. Don't use fixed length buffers when the lengths of inputs to them are highly variable or unknown. Instead, use the string class for variable length character strings, and use the vector class for variable length buffers.

  16. Debugging programs is part science and part art. The science lies in (1) the methodical characterization of program behavior; (2) the development of hypotheses about why certain behaviors are or are not happening; (3) making predictions based on those hypotheses; and (4) designing and conducting (sometimes simple) experiments to evaluate those predictions and the underlying hypotheses. The art lies in creating good hypotheses, predictions, and experiments that lead you quickly and reliably toward an answer, and in thinking creatively about what could happen. For more on this process, see the Wikipedia Scientific method page.

    One very helpful question to ask throughout the debugging process is "what was the last point where my program seemed to be working correctly, and what was the first point where it seemed to be working incorrectly?" A debugger is in invaluable tool in partially automating this process.

  17. Every program should have a "usage" message. It should be printed out if erroneous command line arguments, or a --help command line argument, are provided to the program. In addition to showing the correct format for command line arguments, the usage message should briefly explain what the program does.

B. Program Design and Coding Idioms

    Problems with use of coding idioms can result in either major deductions (for example, causing a memory leak by failing to call delete on memory allocated by new -- even better would be to use safe memory management idioms to avoid this problem) or minor deductions (for example, unnecessarily zeroing out a pointer before calling delete on it) during grading.

  1. Whenever possible (which means always, in this course) use new and delete instead of malloc and free to allocate and deallocate memory, respectively.

  2. Whenever you use new to allocate memory in a program, make sure to find and document where in the program that memory will be freed with a corresponding delete. The best way to do this is to use the safe memory management idioms we'll talk about as the course evolves. A more general way to state this point is that you should always know (and comments in your code should explain where appropriate) the lifetime of any dynamically allocated piece of memory.

  3. Never use free to deallocate memory that was allocated with new. Similarly, never use delete to deallocate memory that was allocated with malloc.

  4. Don't check for a pointer being 0 before deleting it. It's always safe to delete a 0 pointer. If the pointer is visible outside the local scope, it's often a good idea to set it to 0 after deleting it.

  5. Never compare or assign a pointer value with NULL; use 0 instead. The language allows any pointer to be compared or assigned with 0. The definition of NULL is platform dependent, so it is difficult to use it portably.

  6. Avoid using dynamic allocation when using a local variable (also known as an automatic variable) or class member variable instead would do. If something has a lifetime that is contained within the scope of a single function then dynamic allocation should only be used when absolutely necessary, such as when the size is determined dynamically at run-time according to the parameter value(s) passed to that function. Similarly, if something is dynamically allocated in the constructor and dynamically deallocated in the destructor, then there must be some reasonable design force that motivates this approach rather than using an object directly as a member of the class (which your readme file should of course please explain).

  7. Don't use exit() to end the program in the middle of a class method, or in or any method that could be called from another method. This interferes with the normal graceful shutdown of the program, and the release of resources the program acquired during its execution. It's better to use exceptions or return values to cause the program to unwind its call stack and exit from its "main" function.

  8. If a class has any virtual functions, and its destructor is declared explicitly in the class, then the destructor should always be virtual as well.

  9. Avoid default arguments unless there's a very good reason to use them.

  10. Items in the constructor's base/member initialization list should appear in the same order as the data members are declared in the class header, and base class initializations should appear before data member initializations. This helps avoid subtle errors, because initialization takes place with base class constructors being called first, and then data member constructors in the order of member declaration.

  11. Don't initialize class members in the constructor body when it is possible to intialize them in the base/member initialization list.

  12. Don't use functions that could fail in a constructor's base/member initialization list: instead use a "safe" constructor for each base class or member (possibly a do-nothing default constructor) and then call the function in question in the constructor body, where the function's return value can be checked, exceptions can be caught, etc.

  13. Initialization is usually cleaner than assignment, especially in a conditional. So, instead of writing code like this:
            ssize_t n_bytes;
    
            if ((n_bytes = foo.send ((char *) &reply_port,
              sizeof reply_port)) == -1) // ...
    
    Write it like this:
            ssize_t n_bytes = 
              foo.send ((char *) &reply_port, sizeof reply_port)
    
            if (n_bytes == -1) // ...
    

  14. Be aware when initialization of a static variable occurs. A static variable is only initialized the first time its initialization definition is seen, according to the order in which compilation units (source files) are compiled.

  15. It is usually clearer to write conditionals that have both branches using a positive (non-negated) condition. For example,
            if (test)
              {
                // true branch
              }
            else
              {
                // false branch
              }
            
    is preferred over:
            if (! test)
              {
                // false test branch
              }
            else
              {
                // true test branch
              }
    

  16. (Thanks to Prof. Ken Goldman for this one) Avoid unnecessary use of conditional logic. For example simply say
            return condition;
    
    instead of
            if (condition)
              {
                return true;
              }
            else
              {
                return false;
              }
    
    or
            return condition ? true : false;
    

  17. If a type cast is necessary, avoid use of C-style casts, e.g., (int) foo. Use standard C++ casts, e.g., static_cast<int> (foo), instead.

  18. If instances of a class should not be copied, then a copy constructor and assignment operator should be declared as private to the class, but then not defined.

  19. If instances of a class should not be created on the program call stack, but only through dynamic memory allocation, then declare and define a private (or possible protected) destructor. If it's necessary to be able to create class instances from outside the class (or its derived classes), then provide a (possibly static) member function that creates instances of the class using new. If it's necessary to be able to destroy class instances from outside the class (or its derived classes), then provide a member function that destroys instances of the class using delete.

  20. Never use BOOL, or similar types, even if a compiler supports them. Use the standard C++ bool type for Boolean variables, instead.

  21. If a loop index is used after the body of the loop, it must be declared before the loop. For example,
            size_t i = 0;
            for (size_t j = 0; file_name [j] != '\0'; ++i, ++j)
              {
                if (file_name [j] == '\\' && file_name [j + 1] == '\\')
                  ++j;
    
                file_name [i] = file_name [j];
              }
    
            // Terminate this C-style string.
            file_name [i] = '\0';
    

  22. Prefix operators are generally more efficient than postfix operators. Therefore, they are preferred over their postfix counterparts where the expression value is not used.

  23. Except for very simple expressions, use if (...) else .... instead of the contional ?: operator. Your code will be a lot more readable, and this can help you avoid subtle bugs due to the precedence of ?: compared with other operators in an expression.

  24. When a class provides operator==, it must also provide operator!=. Also, both these operators must be const and return bool.

  25. It's a good idea wrap the body of a for or while loop, or an if statement branch in braces even if it's only a single statement, to avoid surprises when other code or debugging statements are added.

  26. Never include a header file when a forward reference will do, and remember that in many cases templates can be forward referenced too.

  27. check the return value from any call that could return an error code after a failure (e.g., new (nothrow))

  28. use try/catch blocks around any call that could throw an exception after a failure (e.g., new)

  29. Narrow interfaces are better than wide interfaces. If there isn't a need for a function or class member function, leave it out. This eases maintenance, minimizes footprint, and reduces the likelihood of interference when other functions need to be added later.

  30. Distinguish definitions from declarations by placing definitions in source (.cc) files, placing declarations in header (.h) files, and using the #include directive to make declarations visible where needed. Note that some compilers use a different source file extension (e.g., .cpp or .cxx) but for this course we will always use the .cc extension for our source files.

  31. Distinguish declarations for class templates and function templates from declarations for non-template classes and functions by placing them into separate header (.h) files. Also distinguish definitions for class templates and function templates from definitions for non-template classes and functions by placing them into separate source (.cc) files. This will give you better decomposition of your programs overall, and some compilers get confused when template and non-template code is mixed in the same file.

  32. Protect header files against multiple inclusion with this construct:
            #ifndef FOO_H
            #define FOO_H
    
            // Class and function declarations go here...
    
            #endif /* FOO_H */
            

    Files that contain type-parameterized classes should follow this style:

          
          #ifndef FOO_T_H
          #define FOO_T_H
    
          // Class template and function template declarations go here...
    
          #if defined (TEMPLATES_REQUIRE_SOURCE)
          #include "Foo_T.cc"
          #endif /* TEMPLATES_REQUIRE_SOURCE */
    
          #endif /* FOO_T_H */
         

    Notice that some compilers need to see the code of the template, in which case the .cc file must be included from the header file. In that case to avoid multiple inclusions of the template .cc file it should also be protected as in:

          #ifndef FOO_T_CC
          #define FOO_T_CC
    
          #include "Foo_T.h"
    
          // Class template and function template definitions go here...
    
          #endif /* FOO_T_H */
        

  33. In a .cc source code file always include the corresponding header file first, like this:

            
            // This is Foo.cc
    
            #include "Foo.h"
            
            // Any other includes go here...
    
            // Definitions go here...
        

    In this way we are sure that the header file is self-contained and can be safely included from some place else (if it seems like other includes should go first, then they probably belong in the corresponding header file).

  34. If you don't use a declared argument to a function in the function's body, give only the argument's type but not its name in the definition signature, e.g.,
            int main (int, char *[])
            {
              // Rest of the definition of main ...
    
              return 0;
            }
            
  35. Use smart pointers (such as auto_ptr) whenever possible, in preference to using new and delete directly.

C. Program Style and Documentation

    Problems with program style and documentation usually result in only minor deductions during grading, but even minor deductions can add up quickly if there are a lot of them. In the longer term, practicing a consistent and high quality programming style will make your code more accessible to others, which is essential in team programming environments.

  1. Always put a comment at the start of each source or header file, with (1) the name of the file, (2) the names and e-mail addresses of the authors of the code in that file (for this course it will only be you), and (3) a brief summary of what the file contains (for example, "(Lab 0) definitions for the namehash program, which computes a simple hash value based on program argument strings.")

  2. Use comments and whitespace generously. Comments should summarize what each non-trivial section of code does, and indicate why you designed it the way you did.

  3. Avoid leaving "commented out" code fragments in submitted code - remove these or use conditional compilation to compile them in/out (by default they should be compiled out).

  4. Avoid commenting things that are already fairly obvious from the code (e.g., begin and end of a method).

  5. Always give a pointer (or size) arithmetic formula a comment explaining it: in general any non-obvious code should have a comment giving an intuition of what it does in general terms (and even better explaining why or how it does it that way).

  6. Avoid unnecessarily cramping symbols or lines together, reducing readability (use lots of whitespace: it's free!)

  7. for, if, switch, and while statements should have a space after the respective control keyword, as in:
            for (unsigned int i = 0; i < count; ++i)
               ++total;
          

  8. Avoid using hard coded constants like 7: instead use an appropriately named constant variable (or if absolutely necessary a precompiler constant) like
         static const int days_in_a_week = 7;
    

  9. Avoid including the string "Error" in a source code filename. GNU Make's error messages start with "Error". So, it's much easier to search for errors if filenames don't contain "Error".