Guidelines in Source Code Design


Usage of References

References should only be used in the following situations:
  • Operator overloading.
  • Using function arguments to return values to callers.
  • Improving performance by replacing value arguments or return types with const references.
  • The need to return a pointer to a caller without having the caller store that pointer (for example if the memory referenced by that pointer may be moved or released). In this case the name of the function that returns such references should start with `ref` instead of `get`.

Singleton and Static Variables

Singletons should be implemented as a class with a single instance rather than a static class (class containing only static members). This is because the sequence in which static variables are initialized is not guaranteed and may lead to a race condition. With single global instance, race conditions may be avoided by delaying the initialization of such instance until it's accessed.

Access Between Singletons

In case of the need for singletons to interact with each other (by keeping a pointer, or receiving it as a function argument), the following rule is followed:
  • The more specialized object can keep a pointer to the more generic object, while the more generic should receive a pointer to the more specialized during the operation.
  • The object where an operation is initiated is the one that can keep a pointer to the other, while the latter should receive the pointer to the first as an arg. It's usually the case that the more specialized is where an operation is initiated.
  • In case of composition, the composed object is the one that keeps the pointer to the composing unit, while the unit should receive a pointer to the composed as an arg.
  • Two objects can keep pointers to each other if they are at the same level of specialization and the upper rules do not apply to them.

Consistency

We should guarantee consistency in code design and naming. Following are some examples:
  • If a wrapper class contains a casting operator for the wrapped class, it's expected that a different wrapper class also contains a casting operator.
  • If a searching function starting with `find` can return nulls, then a different searching function also starting with `find` is expected to be able to return nulls as well.
  • Function names should be indicative of their functionality. For example, a function that returns a member of an array using the member's index should start with `get` and not `find`.
  • If class `map` contains function `remove` then class `list` is also expected to have a `remove` function.

Grouping Related Elements Inside The Same Scope

As much as possible, we should keep related elements inside the same scope. For example:
  • We should destroy objects in the same scope in which they were created. For example, if a member function allocated memory, then releasing that memory should be done inside a member function within the same class.
  • If we add a new linked list member inside some class, then removing that element from the linked list should be done inside the same class.
  • If a container class directly accesses variables of a contained class (and not through contained class's functions) then initializing those variables is the responsibility of the container class.

Plain Pointers and Smart Pointers

Choosing between plain and smart (shared) pointers should be done according to the following criteria:
  • The rule is to use smart pointers for any non-temp variable (temp variables are function args and local variables). Exceptions are allowed in the following cases:
    • External pointers, i.e. those obtained from external libs.
    • Performance improvement cases.
    • Pointing to an owner object where the pointer will always be valid because the destruction of the owner leads to the destruction of the owned and because using smart pointers leads to a circular smart pointer.
    • Pointers to singletons.
  • Plain pointers are used for temp variables like function args and local varialbes. Exceptions are allowed in the following cases:
    • Receiving pointers to keep.
    • Property accessor functions where the property is a smart pointer. In this case a reference to a smart pointer is returned.
    • Returning a just-created object (that will be stored by the caller).

`const String&` vs `const Char*`

`const Char*` should be used for function arguments. The reason is to avoid instantiating new String objects when we can pass simple char pointers.
`const String&` should be used for function return values. This is to enable the advanced functionalities of String on function return values. If using String in return values requires instantiating a new String object then we should use Char* instead of String.

Exceptions

Exception usage should be limited to exceptional errors and should never be used in program control. For example, if it's normal for a search function to return no results then dealing with no-result situations should be done through return values rather than exceptions. On the other hand, if the program faces syntax errors while parsing configuration files then exceptions should be used to deal with this situation because finding an error in config files is not expected.
Naming convention should also be respected when dealing with exceptions. For example, a search function that is expected to always find results should start with `get` while a search function that can find no results should instead start with `find`.

Nested Types

Nested types (types defined inside a class) follows the following rules:
  • If a type is internal to a class (not used outside the class) it is defined inside that class.
  • If a type is used only with a certain class and is not used outside of it except for dealing with it, then it's defined inside that class. An example of this is return types of a member function.
  • If the type is related to a class but can be used outside the class independently of it, then it's defined outside the class.
  • If the type is a class that can be a parent of another non-internal class then it's defined outside any class.

Miscellaneous Notes

  • `std::vector` should store pointers when the stored object contains a complex constructor (for example one that allocates memory) in order to avoid repeatedly calling this constructor when the objects are copied during vector resizing.
  • Member functions that don't modify any member variables should always be marked as const.
  • We should avoid global variables as much as possible.
  • Variables should be instantiated in the smallest scope possible.
  • Structures should not be used except when it doesn't need member functions. An exception to that is constructor functions.
  • Initialization lists should be used during object construction instead of assignment operations.
  • We should follow SOLID principles for object-oriented design.

Naming Conventions


Grammar Naming Conventions

Names of Tokenizing Rules, Parsing Rules, and Character Groups

Written with Pascal style, i.e. with an upper case letter at the begining of each word as in: ImportCommand.

Module Names

Written with Pascal style as well, like tokenizing rules. As in: ImportSubject.

Data Names

Data, like string or integer or list or map, is written with camel style, i.e. with an upper case letter at the beginning of each word other than the first word. As in: multipllicationOperators.

Type Names

Types are written with lowe case words separated by underscores, as in: valid_subject.

Source Code Naming Conventions

Keywords

Written with lower case words separated by underscores, as in: my_keyword.

Classes, Interfaces, Enumerations, and Namespaces

Written in Pascal style, i.e. with an upper case letter at the beginning of each word, as in: MyClass, MyNamespace.
Interface names use verbal nouns while class names use agent nouns. For example, an interface can be called "Writing" while the class implementing it is named "Writer".

Variables

Written in camel style, as in: myVar.

Functions

Written with the same style as variables, but they should start with a verb, as in: getMyVar.

Signals

Written with the same style as variables, but they end with the word `signal` or `inquirer` or `notifier`. As in: tokenProcessingNotifier, tokenProcessingSignal, tokenProcessed.

Macros and Constants

Written in all upper case words separated by underscores, as in: MY_CONSTANT, MY_MACRO.

Template Arguments

Written similar to constants, i.e. with upper case words separated by underscores. Ex: TEMPLATE_ARG.

Dependent Elements

Dependent elements are elements that should be accessed directly; instead, they should be accessed through other elements. Dependent elements should start with an underscore. For example, the function _getValue should not be called directly, instead the getValue function should be called.

Miscellaneous Notes

  • Names are not prefixed with any notation. For example, pointers don't start with the letter p and global variables don't start with the letter g or g_.
  • Functions that enquire about a boolean value should start with `is`, as in `isVisible`, or starts with a third party simple present verb as in `exists`.
  • Negative names should be avoided. For example, we should use errorFound instead of errorNotFound.
  • Enumeration values can start with the name of the enumeration or a short version of it. As in:
    enum Color {
        COLOR_RED,
        COLOR_GREEN,
        COLOR_BLUE
    };
    
  • Class name is implied and should not be repeated in member elements. For example:
    String::getLength()        // correct
    String::getStringLength()  // wrong
    
  • Plural form is used with elements that hold multiple objects as in arrays for example.
  • Abbreviations are treated like words and are not written with all upper case letters. For example, end-of-file in function names is written as Eof rather than EOF.

File Naming Conventions

  • Filenames are not prefixed with any notation.
  • Folder names start with upper case letters and their words are separated by underscores.
  • Filenames are written with lower case words separated by underscores.
  • Folder and file names that relate to source code elements match their names and in the same form. For example, the file that contains the definition of class MyClass should have the name MyClass and not my_class or My_Class.
  • Namespaces are represented by folders except in situations where the entire namespace can be placed inside a single file in which case the folder is dropped and the file carries the name of the namespace.
  • Source files have the cpp extension while header files have the h extension.

Source Code Layout


Classes and Functions

Opening and closing brackets are placed on separate lines.
class MyClass
{
    ...
};
void myFunction()
{
    ...
}

Structures

Structures can have one of the following two forms:
struct MyStruct
{
    ...
};
struct {
    ...
} myVar;

Namespaces

Opening and closing brackets are placed on separate lines and, unlike classes, the contents of namespaces are not shifted.
namespace MyNamespace
{

class MyClass
{
    ...
}; // class

} // namespace
To save space, we can write nested namespaces in the following form:
namespace OuterNamespace { namespace InnerNamespace {

class MyClass
{
    ...
}; // class

} } // namespace

Loops

Opening bracket is placed on the same line as the loop statement and the closing bracket is placed on a separate line. A space must be added after the loop keyword, and another space must be added before the opening bracket.
for (...) {
    ...
}
while (...) {
    ...
}
If the body of the loop consists of a single statement we can drop the brackets and put the statement on the same line as the loop command. If the body is longer than to be placed on the same line then the brackets should be added even if the body is a single statement.
                   for (...) doSomething();
                   

Conditional Statements

Conditional statements have the same layout as loop statements. If we have an `else` clause then it should be placed ont he same line as the closing bracket and the next opening bracket, with spaces before and after the `else` keyword.
if (...) doSomething();
if (...) {
    ...
}
if (...) {
    ...
} else if (...) {
    ...
} else {
    ...
}

Switch Statements

Similar to loop statements. The `case` statements are shifted once, while the body of the `case` statements are shifted twice.
switch (...) {
    case ...:
    ...
    break;
    case ...:
    ...
    break;
}

Exceptions

Try-catch statements have the following layout:
try {
    ...
}
catch (...) {
    ...
}
catch (...) {
    ...
}
finally {
    ...
}

Blank Lines

  • No need to separate variable definitions with blank lines.
  • Inline functions are separated by a single blank line, while non-inline functions are separated by two blank lines.
  • Different sections of a class are separated by two blank lines that are placed before the title of the section. The title is then followed by a single blank line.
  • A single blank line is added before the closing bracket of a namespace.
  • The namespace definition statement is preceeded by a single blank line and followed by another blank line.

Comments

  • Souce code elements should be documented with doxygen comments and using javadoc style.
  • Different sectiosn of the same file should be separated by a series of / character that reaches up to column 80.
    /**
    * @file My_Class.h
    * .....
    */
    //////////////////////////////////////////////////////////////////////////////
    
    class My_Class
    {
        ////////////////////////////////////////////////////////////////////////////
        // Member Variables
        ...
    
        ////////////////////////////////////////////////////////////////////////////
        // Constructor / Destructor
        ...
    
        ////////////////////////////////////////////////////////////////////////////
        // Member Functions
        ...
    }; // class
    
  • Sections of a doxygen comment block are separated by a single blank line. Function inputs and return value are treated as a single section. The brief line and the @ingroup statement are considered one section. Description can have multiple sections. Example:
    /**
    * @brief This is the brief.
    * @ingroup samplegroup
    *
    * This is the first
    * details block.
    *
    * This is the second
    * details block.
    *
    * @param param1 ...
    * @param param2 ...
    * @return ......
    *
    * @note .....
    *
    * @sa ...
    * @sa ...
    */
    
  • We can drop the blank lines between different sections of a doxygen comment if each of these sections is a single line only.
  • The symbol <br> should be used if we want to start a new section in a doxygen comment.

Miscellaneous Notes

  • Members of a class are defined in the following order: Types followed by member variables followed by constructors and destructors followed by member functions.
  • Keywords of commands are always followed by a single space character.
  • Maximum width of a line in the source code is 120 columns.
  • Tabs are represented by two spaces. The tab character should be avoided.
  • When a line is broken into multiple lines, the next line should start from the same point to which the new line belongs. For example:
    result = a + b + c + d + e +
            f + g;
    void myFunction(int arg1, int arg2, int arg3,
                    int arg4, int arg5);
    for (int i = 0;
        i < 5;
        i++) {
        ...
    }
    
  • Pointer symbols (*) should be adjacent to the pointer's name rather than the pointer's type in order not to give the impression that its effect applies to what's after the comma (in multi variable definitions). For example:
    int *i, j; // correct, because i is a pointer, but not j.
    int* i, j; // wrong
    
    The opposite applies for function return types in order not to be confused with pointer to functions.
    int* getSomething(); // correct
    int *getSomething(); // wrong
    
  • Include statements should always be placed at the beginning of source files.
  • Global members should be referenced using :: or <namespace>::. We can exclude members of the Basic namespace like smart pointers or casting operations.
  • Members of a class are always referenced with this->.
  • Static members of a class are always referenced with <class name>::.
  • Function bodies should be placed in cpp files, not header files, except in the following situations:
    • Inline functions: always go into header files.
    • Very simple functions (one or two lines with no loops): No preference.
    • Relatively simple funciton (few statements that may include simple loops) and all other member functions are either simple or inline: No preference.
  • Initializing loop variables should be done immediately before the loop.
  • Infinite loops should be done with: while (true).
  • It's not allowed for anything other than loop controlling expressions to appear inside the brackets of a loop command. For example:
    // Correct:
    total = 0;
    for (i = 0; i < 10; ++i) {
        total += arr[i];
    }
    
    // Wrong:
    for (total = 0, i = 0; i < 10; ++i) {
        total += arr[i];
    }
    
  • We should replace do-while loops with regular while loops if possible because the latter is easier to read.
  • Read numbers are always written with the decimal point. For example 5 should be written as 5.0. The zero should not be dropped in fractional numbers like 0.5.
  • Avoid dropping function return type even if the compiler supports that.
  • Type casting should be explicit and not implicit.
  • Avoid treating non-binary values as binary values in conditional statements. For example:
    if (str == 0) ... // correct
    if (!str) ...     // wrong
    
  • Definitions related to a certain class are placed inside the header of that class, even if they are not internal to the class itself.