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 and only) the files needed to evaluate your solution. Deductions may be made for including extraneous automatically-generated files or for omitting necessary files from the submitted code, and the deductions will increase in cost with the number of occasions on which submissions include superfluous files or are missing files. A good way to make sure all the files are submitted is to check what you are about to turn in: (1) if you are working on Windows, are all the files you added, the main file, and the ReadMe.txt file (but not any of the automatically generated files like stdafx.h) present in the zip file you are going to submit? (2) if you are working on Linux, are they all listed in the Makefile, 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" (on Windows "ReadMe.txt") file that documents what the submitted program does, any places where your solution diverged from what was requested in the assignment, and the design decisions you made in developing your program. Please also ask any remaining questions you may have, 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 most stringent warning level that won't complain about the libraries provided by the development environment (e.g., with g++ on Linux use the -Wall switch) to see all possible compiler warnings about your code, and then fix all those 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, both in your readme file and in your code.

  6. Your program's main function must return 0 on successful termination, and a unique non-zero value for each different kind of failure otherwise: please avoid using the same non-zero return value for different kinds of errors. With a number of different return values are provided, it's also good practice to document what the different return values mean through (1) giving the return values descriptively named labels (see guideline below about not using hard coded numbers); (2) providing comments in the program source code; and (3) describing the kind of error that occurred (and how to avoid it) in the usage message(s) and error messages the program generates.

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

  8. 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.

  9. 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.

  10. After calling a function that can fail (for example, constructing a file stream object with a file name, or otherwise 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 (in which case you should please provide a comment in the code and in the design discussion in your readme (ReadMe.txt on Windows) file explaining why not.

  12. Don't dereference a pointer whose value is zero or a pointer or array index 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, and will result in a major deduction if it occurs during grading.

  13. Be very careful with array index or pointer arithmetic, and ensure that any array index or 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 array index and 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 array index and 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 other 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 helpful "usage" message. It should be printed out if erroneous command line arguments (or too many or too few) are provided to the program, and should show the correct format for command line arguments.

B. Program Design and Coding Idioms

    Problems with use of coding idioms can result in either major deductions (for example, for causing a memory leak by failing to delete memory that was allocated by new) or minor deductions during grading.

  1. Don't use exit() to end the program in the middle of a function or class (or struct) method. Doing so 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 return an appropriate value (indicating success or failuer) from its main function, and it can be especially helpful to use smart pointers in conjunction with this approach to ensure dynamically allocated resources are cleaned up within appropriate scopes.

  2. Avoid default arguments unless there's a very good reason to use them. If you do use them, please indicate the reason in a comment.

  3. Never compare a pointer value to (or assign it) NULL; use 0 (or with a C++11 adherent compiler use nullptr) instead. C++ allows any pointer to be compared to or assigned 0 in old and new compilers alike. The definition of NULL is platform dependent in some older compilers, so it may be difficult to use it portably.

  4. Whenever necessary to do allocation and deallocation directly (which should be rare and the need for doing so should always be explained in a comment if so) use new and delete instead of malloc and free to allocate and deallocate memory, respectively.

  5. 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 and smart pointers 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.

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

  7. Don't check for a pointer being 0 before deleting it unless there is a compelling reason for your program to do that (and if there is you should give a comment explaining why). It's always safe to delete a 0 pointer. If the pointer is visible outside the local scope, it may a good idea to set it to 0 after deleting it.

  8. Avoid using dynamic allocation when using a local variable (also known as an automatic variable) or class (or struct) 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 or struct (which comments in your code and your readme file should of course please explain).

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

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

  11. Don't initialize class or struct 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 struct) 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 clearer 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. If you need to use a cast, you also need to provide a comment explaining why.

  18. If instances of a class or struct should not be copied, then a copy constructor and assignment operator should be declared as private to the class or struct (or with a C++11 adherent compiler should be declared with = delete), but then not defined.

  19. If instances of a class or struct 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 or struct instances from outside the class or struct (or its derived classes or structs), then provide a (possibly static) member function that creates instances of the class or struct using new or make_shared. If it's necessary to be able to destroy class or struct instances from outside the class or struct (or its derived classes or structs), then provide a member function that destroys instances of the class or struct using delete, but be careful to make sure there are no remaining aliases to the instance at that point.

  20. Never use BOOL, or similar compiler-specific types, even if a compiler supports them. For portability, always 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 often (at least slightly) more efficient than postfix operators. Therefore, they are preferred over their postfix counterparts where the expression value is not used. If the expression value matters, you should always give a comment to indicate why it matters and why the version of the operator that is being used is most appropriate.

  23. Except for very simple expressions, use if (...) else .... instead of the conditional ?: 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. To avoid surprises when other code or debugging statements are added, it's a good idea to put braces around the body of a for or do or while loop, or an if or else statement branch, even if it's only a single statement. For example:

            for (int j = 0; j < argc; ++j) 
              {
                cout << argv [j] << endl;
              }
    
            int k = 0;
            do 
              {
                cout << argv [k++] << endl;
              } while (k < argc);
    
            while (k > 0) 
              {
                cout << argv [--k] << endl;
              }
    
            if (0 <= k && k < argc) 
              {
                cout << argv [k] << endl;
              }
            else 
              {
                cout << "argument position " << k << "is out of range." << endl;
              }
    
    

  25. To break circular inclusion dependences, in addition to using inclusion guards, selectively replace inclusions of other header files with forward declarations of the items that would have been included, and remember that in many cases templates can be forward referenced too.

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

  27. Either use try/catch blocks around any call that could throw an exception after a failure (e.g., new), or use RAII and other programming idioms to make code exception-safe.

  28. Narrow interfaces are better than wide interfaces. If there isn't a need for public access to a function or class or struct member function, leave it out of the interface (e.g., in a class or struct make it private). This eases maintenance, minimizes footprint, and reduces the likelihood of interference if other functions need to be added later.

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

  30. Distinguish declarations for class or struct templates and function templates from declarations for non-template classes or structs and non-template functions by placing them into separate header (.h) files. Also distinguish definitions for class or struct templates and function templates from definitions for non-template classes or structs and non-template functions by placing them into separate source (.cpp) or (depending on the compiler) header (.h) 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.

  31. Protect header files against multiple inclusion with this "inclusion guard" construct:
            #ifndef FOO_H
            #define FOO_H
    
            // Class or struct and function declarations in foo.h go here...
    
            #endif /* FOO_H */
            

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

          
          #ifndef FOO_T_H
          #define FOO_T_H
    
          // Class or struct template and function template declarations for foo_t.h go here...
    
          #if defined (TEMPLATES_REQUIRE_SOURCE)
          #include "Foo_T.cpp"
          #endif /* TEMPLATES_REQUIRE_SOURCE */
    
          #endif /* FOO_T_H */
         

  32. In a .cpp source code file always include the corresponding header file as the first inclusion (after any environment-specific mandatory inclusions like stdafx.h on Windows with precompiled headers), like this:

            
            // This is Foo.cpp
    
            #include "stdafx.h" // would not have this on Linux
            #include "Foo.h"
            
            // Any other includes go here...
    
            // Definitions go here...
        

    This ensures 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 need to be included in the corresponding header file).

  33. 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;
            }
            
  34. Use the RAII idiom with smart pointers (such as using make_shared and shared_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 modern 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. Whitespace is free and can help make code much more readable than if it is all crammed together. 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) and comment why leaving them in the code is appropriate.

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

  5. Always give a pointer (or size or array index) 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. Just in case you missed it above, avoid unnecessarily cramping symbols or lines together, reducing readability (use lots of whitespace: it's free!)

    For example, 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;
               }
          

  7. Avoid using hard coded constants like 7: instead use an appropriately named constant variable or enum label (or if absolutely necessary a precompiler constant) like one of the following:
         const int expected_argument_count = 3;
         enum command_line_arguments {program_name, input_file, output_file};
         #define SUCCESS 0     
    

  8. Except where they are obviously appropriate (e.g., using "Error" in an error message) avoid including widely used character sequences in other places (e.g., using "Error" in a source or header file's name) to make your code files easier to work with on different platforms and in different development environments. For example, GNU Make's error messages start with "Error", and so it's much easier to search for errors if only error messages (and not filenames, etc.) contain "Error".