CSE 428 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 contribute 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 may result in grading deductions.

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, 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 required for the assignment.

  2. Make sure you submit all the files needed to evaluate your solution. Deductions will be made for files missing from the lab submission as of the deadline for the lab.

  3. Always submit a readme file (named, e.g., "readme.txt") 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 code. Please also ask any remaining questions you may have, in your readme file, and feedback on your impressions of the lab (what was easy, what was difficult, suggestions, etc.) is welcome.

  4. Test your program thoroughly. Don't just run it a few times and assume it will work correctly when it is graded (using a set of tests designed for that purpose). Instead, think about the ways your program might fail, and then design additional tests to explore those cases. Think about 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 iterator, 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 normal 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 command line switch) to see all possible compiler warnings about your code, and then fix all those compiler warnings before submitting your code 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 comments in your code.

  6. Your program's main function must return a value of 0 (ideally by returning a descriptively named variable or label with that value) 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. When different return values are possible, it's also good practice to document what each return value means through (1) giving each return value a descriptively named label (see guideline below about not using hard coded numbers); (2) providing comments in the program header and 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 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 error code value to indicate program failure.

  9. Only use exceptions to unwind the program call stack under abnormal conditions: do not use exceptions (or goto statements, etc.) 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 file explaining why not).

  12. Don't dereference a pointer whose value is zero, or dereference a pointer or array index to a memory location outside those allocated to your program). If you do so your program may end. Abruptly. This is called an access violation, and (like a divide-by-zero error, which also should be avoided) will result in a major deduction if it occurs during grading.

  13. Be 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 a valid object, then (per the previous guideline) 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, viruses, and other exploits often target this weakness of C, C++, and other similarly "unsafe" languages by trying to cause programs to overwrite their call stacks or other crucial areas, in order to give the malicious code control over the computer. If your program seems to be corrupted (this often can be seen readily through tracing in a debugger), it's definitely time to go back and re-check your array index and pointer math and look for statements that may have executed improprerly, up to the point when things became 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 is known to be working correctly, and what was the first point where it is known to be working incorrectly?" A debugger is in invaluable tool in partially automating this process.

  17. Every program whose correct behavior depends on the number, format, or values of the command line arguments provided to it should have a helpful "usage" message. The usage message 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 program design or coding 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 (for example, checking for a pointer being 0 before deleting it without providing a comment for why that was appropriate) during grading. Please familiarize yourself with these guidelines, and the coding idioms they suggest, and apply them as you develop your solutions.

  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 return statements with appropriate values (or in some cases exceptions, though if so give comments why) to cause the program to unwind its call stack and return an appropriate value (indicating success or failure) from its main function. 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 good reason to use them. If you do use them, please indicate in comments in the code the reason(s) why they are needed.

  3. Never compare a pointer value to (or assign it the value) NULL; with a C++11 adherent compiler please use nullptr (or with an older compiler use 0, and then comment why you had to do that) instead. C++ allows any pointer to be compared to or assigned 0 in older and more up-to-date compilers alike. The definition of NULL is platform dependent in some older compilers, so it may be difficult to use it both correctly and portably.

  4. Whenever possible, use containers, smart pointers (and the safe memory management idioms they support), and other abstractions from the standard C++ libraries to manage memory, instead of doing allocation and deallocation directly within your program code.

    For example, use the Resource Acquisition Is Initialization (RAII) idiom with smart pointers (e.g., via make_shared with a shared_ptr) whenever possible, in preference to using new and delete directly.

  5. Whenever it is 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.

  6. 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. 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 memory.

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

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

  9. 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 using this approach (which comments in your code and your readme file should of course please explain) instead of using an object directly as a member of the class or struct.

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

  11. 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 (and compiler warnings if you've told the compiler to be as verbose with warnings as you should per the guideline earlier), because initialization takes place with base class (or struct) constructors being called first, and then data member constructors in the order of member declaration.

  12. Don't initialize class or struct members in the constructor body when it is possible to intialize them safely (see next guideline) in the base/member initialization list instead.

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

  14. Initialization is usually clearer than assignment, especially when it may affect conditional logic. For example, 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 instead:
            ssize_t n_bytes = 
              foo.send ((char *) &reply_port, sizeof reply_port);
    
            if (n_bytes == -1) // ...
    

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

  16. 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
              }
    

  17. (Thanks to Prof. Ken Goldman for this one) Avoid unnecessary use of conditional logic. For example, because a condition has (or produces) a typed value, often you can simply say
            return condition;
    
    instead of
            if (condition)
              {
                return true;
              }
            else
              {
                return false;
              }
    
    or
            return condition ? true : false;
    

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

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

  20. 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 in some special cases 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 make_shared (or possibly new for an older compiler). 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, which if dereferenced could cause the program to crash (or even corrupt the program's state).

  21. Never use BOOL, or similar platform-specific or compiler-specific types, even if a compiler supports them. For portability, always use the standard C++ bool type for Boolean variables, instead.

  22. 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';
    

  23. 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 increments operator that is being used is most appropriate.

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

  25. 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;
              }
    
    

  26. To break circular inclusion dependences, in addition to using #pragma once with a C++11 adherent compiler (or with older compilers using #ifndef ... #define ... #endif 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 declared as well.

  27. Always check the return value from any call that could return an error value if it fails (e.g., new (nothrow)).

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

  29. Narrow interfaces are better than wide interfaces. If there isn't a need for public access to a function (or a 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 conflicts if existing functions are modified or if other functions need to be added later.

  30. 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 different source file extensions (e.g., .cc or .cxx) but for this course we will prefer the .cpp extension for source files.

  31. Distinguish declarations of 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 separate 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 different source (.cpp) files. This will give you better decomposition of your programs overall, and some compilers may be confused when template and non-template code is mixed in the same file. One nice naming convention to distinguish files that contain template declarations or definitions from those that do not, is to put _T at the end of their names (just before the file extension).

    Note that some compilers, like g++, require the inclusion of template definitions following their declarations. However, this does not mean that you should put those definitions into the header files with the template declarations. Instead, for portability of the code across platforms and compilers, at the end of the header file put a conditional inclusion guard and inside it include the appropriate template source file, and then pass the guard symbol to the compiler via a command line switch (e.g., -DTEMPLATE_HEADERS_INCLUDE_SOURCE for g++). For example, the end of Foo_T.h would say:

            #ifdef TEMPLATE_HEADERS_INCLUDE_SOURCE /* test whether guard symbol is defined */
            #include "Foo_T.cpp"
            #endif
    

  32. Protect header files against multiple inclusion using #pragma once, or for older compilers with an "inclusion guard" construct, as in:
            #ifndef FOO_H
            #define FOO_H
    
            // Class or struct and function declarations in foo.h go here...
    
            #endif /* FOO_H */
    

    Per the previous guidelines, 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 (TEMPLATE_HEADERS_INCLUDE_SOURCE)
          #include "Foo_T.cpp"
          #endif /* TEMPLATE_HEADERS_INCLUDE_SOURCE */
    
          #endif /* FOO_T_H */
         

  33. 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" // Windows only, would not have this line 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).

  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.,
    
            const int SUCCESS = 0;
            
            int main (int, char *[])
            {
              // Rest of the definition of main ...
    
              return SUCCESS;
            }
            

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 (and to you after you've been away working on other code for a while), 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, and (3) a brief summary of what the file contains (for example, "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 (e.g., if you intend to use them for debugging purposes in a subsequent lab assignment).

  4. Avoid commenting on things that are already fairly obvious from the code itself (e.g., where a method begins or ends).

  5. Always give any non-trivial formula involving pointer (or size, or array index) arithmetic 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 and/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".