Skip to content

Coding standard

Simon Renger edited this page Jan 19, 2020 · 3 revisions

Introduction

The following coding standard/guide is based on the following coding standards:

The main purpose of these rules is to guarantee that all the provided code is uniform. Next to this, these guidelines will encourage us to develop readable, usable, maintainable, and stable source code.

  • Readability: Similar to a well-written book, source code should be pleasurable to read. You will not be the only person reading your code. You should write your code in a way that others will be able to read and understand what it is you’re are trying to accomplish.
  • Usability: You should write your code with the intention that it will be useable by others. The source code you create should be easy to use correctly, but hard to use incorrectly.
  • Maintainability: Your source code should be easy to maintain. The source code is a living entity. It rarely remains static after writing it the first time. It should be easy to update and improve your classes and data structures.
  • Stability: Your source code should be stable and robust. Checking pre- and post-conditions in functions, writing testable functions and classes, and validating that your algorithms are performing correctly help to guarantee the robustness of your code.

You should never assume that the source code that is written by you will only ever been seen by you. Any piece of software you write while working on a group project (even prototype or experimental) may find its way into the main code-line. It is easier to simply follow the programming standard for everything you write rather than reformatting the code afterward to match the standard. Perhaps the only exception to this rule is when writing code on paper which you intend to throw away immediately.

Consistency

Our code base is a from-scratch codebase this means we can enforce this coding standard from the beginning. We will not mix this coding standard with others unless it is part of a third-party library.

The standard

We all agreed to make use of the C++ Core Guidelines for guidance. We will try to follow them as much as possible.

Conventions

Terminology

About Parenthesis, Braces, Brackets, and Angle Brackets, Tilde:

  • Parenthesis ( ) : The term “parenthesis” refers to the curved enclosing characters. The (character is referred to as opening parenthesis or left parenthesis and the ) character is referred to as closing parenthesis or right parenthesis.
  • Braces { } : The term “braces” is used to refer to the curly enclosing characters. The { character is referred to as opening brace or left brace and the } character is referred to as closing brace or right brace.
  • Brackets [ ] : The term “brackets” refers to the square enclosing characters. The [character is referred to as opening bracket or left bracket and the [ character is referred to as closing bracket or right bracket.
  • Angle Brackets < > : The term “angle brackets” refers to the less-than and greater-than enclosing characters. The < character is referred to as opening angle bracket or left angle bracket and the > character is referred to as closing angle bracket or right angle bracket. When these characters are used in the context of a logical operator they will be referred to as less-than symbol and greater-than symbol.
  • The tilde ~ character is used to denote the destructor of a class and also as a bitwise inversion operator. It is usually found at the top-left side of your keyboard above the Tab key and the left of the 1 key.

Source: Breda University of applied science CMGT coding standard.

Naming

  1. In general, we should make use of the English language with American spelling.
  2. Names should be clear and self-explanatory
  3. Core Guidelines about naming

Naming rules

Class, Struct Function

The following conventions have to be applied:

  • class,struct and function have to be in Upper Camel Case

A naming convention, also known as PascalCase, in which several words are joined together, and the first letter of every word is capitalized. Contrast this with LowerCamelCase, in which the first letter of the entire word is lowercase, but subsequent first letters are uppercase.

Source

Variables

For variables the following rules have been followed

  • Variables follow the Lower Camel Case rule: AuthCode userAuthCode;

A naming convention of the CamelCase family in which several words are joined together, where the first letter of the entire word is lowercase, but subsequent first letters are uppercase.

  thisIsAnExample
  ThisIsNotAnExample

Contrast this with UpperCamelCase (also known as PascalCase ), where the first letter of every word is capitalized.

Source

Member Variables

Data members of classes, both static and non-static, are named like ordinary nonmember variables.

class TableInfo {
  ...
 private:
  string tableName;  // OK - underscore at end.
  static Pool<TableInfo>* pool;  // OK.
};
Private Member Variables

which are not supposed to be exposed to the outside have the sufix _

class FakeGlmVec3Wrapper
{
    fake_glm::fake_vec3 vec3_;
public:
    FakeGlmVec3Wrapper() : vec3_{}
    {
        std::cout << "Empty Wrapper constructor...\n";
    }
    [...]

Example & proposed by Maiko

Struct Data Members

Data members of structs, both static and non-static, are named like ordinary nonmember variables.

struct UrlTableProperties {
  string name;
  int numEntries;
  static Pool<UrlTableProperties>* pool;
};
Global variables

start with a g_ prefix

string g_tableName;
Static variables

start with a s_ prefix

static string s_tableName;
Enumerations:
  • Values are completely All caps (Uppercase).
  • Names are Upper Camel Case
Defines

Defines are written in all caps. Words are separated by underscores.

Interfaces

Although C++ doesn’t really support real interfaces, classes that have no real functional implementation and are designed to serve as an interface, are prefixed with an ‘I’.

class IBot{
    virtual ~IBot(){}
    
    virtual int GetLife() = 0;   
}

Spacing and tabs

Set your IDE to use 4 spaces when pressing the Tab key on your keyboard. Make use of spacing to indicate logical operation such as loops or if statements

This makes the intent clearer.

Do not split statements over lines unless necessary. This rule also holds for (member) function declarations. Include the return type on the same line as the function name, just like function definitions.

For constructors with initializer lists, each initialization is placed on a separate line and indented, while the opening brace is placed on the line below the last initializer, aligned with the character position of the constructor.

This rule makes adding breakpoints easier.

Example:

scope

void Foo()
{
	if (bar){
 	// ...
 	}
}

spacing between functions

void Foo()
{
	// ...
}

void Bar()
{
	// ...
}

switch

switch (a_Message)
{
    case WM_QUIT:
    {
         // ...
    break;
    }
    case WM_CREATE:
    {
         // ...
    break;
    }
} 

function definition

int main(int a_Argc, char* a_Argv[])
{
	// ...
} 

Operator Spacing

Use a space in front and behind all binary operators. Use a single space in front of a unary operator.

int x = y + 10 * z << d;
a += -b;

Comma, colon and semicolon spacing Use a space after a comma, colon, and semicolon, but not in front.

for (int x = 0, y = 0; x != m_Width, y != m_Height; ++x, ++y)
{
} 

Pre- and postfix increment/decrement spacing Don’t use a space after a prefix increment (++i) or decrement (--i) and before a postfix (i++) or decrement (i--).

Template spacing Declaration of template parameters is done on a separate line(s)

template<typename TYPE>
class Container
{
public:
    template<typename TYPE>
    void Insert(TYPE &a_Value){
     	// ...
     }
    template<typename TYPE, typename TYPE2>
    void Insert(
            TYPE &value
        	TYPE2 &value2
        ){
     	// ...
     }
    template<
    	typename TYPE, 
    	typename TYPE2,
    	typename Alloc = std::allocator<TYPE>>
    void Insert(
            TYPE &value
        	TYPE2 &value2
        ){
     	// ...
     }    
}; 

Include order

This is the order in which files should be included.

  1. precompiled header, if any
  2. headers from the currents module
  3. headers from libraries, sorted by SDK
  4. system headers

Comments

In order to make documentation possible comments are required. The standard for comments is as following

Long descriptions

This is the default for long descriptions:

/**
* 
* Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod
* tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero
* eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea
* takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, 
* consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et 
* dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo 
* dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem 
* ipsum dolor sit amet.
*
*/

Its not required but nice to see if you make use of doxygen friendly commands such as you know from JavaDocs

It is required to structure your paragraphs

/**
* Introduction:
* Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod
* tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero
* eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea
* takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, 
* 
* Details:
* consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et 
* dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo 
* dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem 
* ipsum dolor sit amet.
*
*/

Short comments

If you have to say something small make use of this:

//Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod

Comments should be always above the thing they are describing.

Where and When

Comments should be set in the .h (header file) above the function, method, on top of the document. They should not be set where no explanation is needed. They should help to make use of the class Interface.

//bad
/**
* This getter returns an int which is the size of [...]
**/
int GetSize();
/// Good

/**
* This getter returns an int which is the size of [...] which is multiplied with 42
**/
int GetSize();
[...]
//In cppFile
int MyClass::GetSize(){
    return m_size * 42;
}

General speaking in .cpp files we are only using comments if the intention is not clear or we want to justify why we do something the way we do it.

West const or East const

We are going with the west const approach:

void func(int const * const a )

Defines

Do not use defines for named constants; there is no need to use defines for names constants in C++. Defines do not respect scope and complicate debugging.

Bad

#define MAX_ANGLE_RADIANS 1.5f
#define ReleaseBuildX64

Good

const float g_MaxAngleRadians = 1.5f;
#define RELEASE_BUILD_X64

Even better

constexpr float g_MaxAngleRadians = 1.5f;
#define RELEASE_BUILD_X64

Range based loops

Make use where ever applicable use of range-based for loops.

T.123: Use constexpr functions to compute values at compile time

Reason

A function is the most obvious and conventional way of expressing the computation of a value. Often a constexpr function implies less compile-time overhead than alternatives.

Pointers and References

Ownership and Smart Pointers

Prefer to have single, fixed owners for dynamically allocated objects. Prefer to transfer ownership with smart pointers.

"Ownership" is a bookkeeping technique for managing dynamically allocated memory (and other resources). The owner of a dynamically allocated object is an object or function that is responsible for ensuring that it is deleted when no longer needed. Ownership can sometimes be shared, in which case the last owner is typically responsible for deleting it. Even when ownership is not shared, it can be transferred from one piece of code to another.

"Smart" pointers are classes that act like pointers, e.g. by overloading the * and -> operators. Some smart pointer types can be used to automate ownership bookkeeping, to ensure these responsibilities are met. std::unique_ptr is a smart pointer type introduced in C++11, which expresses exclusive ownership of a dynamically allocated object; the object is deleted when the std::unique_ptr goes out of scope. It cannot be copied but can be moved to represent ownership transfer. std::shared_ptr is a smart pointer type that expresses shared ownership of a dynamically allocated object. std::shared_ptrs can be copied; ownership of the object is shared among all copies, and the object is deleted when the last std::shared_ptr is destroyed.

Pros:

  • It's virtually impossible to manage dynamically allocated memory without some sort of ownership logic.
  • Transferring ownership of an object can be cheaper than copying it (if copying it is even possible).
  • Transferring ownership can be simpler than 'borrowing' a pointer or reference because it reduces the need to coordinate the lifetime of the object between the two users.
  • Smart pointers can improve readability by making ownership logic explicit, self-documenting, and unambiguous.
  • Smart pointers can eliminate manual ownership bookkeeping, simplifying the code and ruling out large classes of errors.
  • For const objects, shared ownership can be a simple and efficient alternative to deep copying.

Cons:

  • Ownership must be represented and transferred via pointers (whether smart or plain). Pointer semantics are more complicated than value semantics, especially in APIs: you have to worry not just about ownership, but also aliasing, lifetime, and mutability, among other issues.
  • The performance costs of value semantics are often overestimated, so the performance benefits of ownership transfer might not justify the readability and complexity costs.
  • APIs that transfer ownership forces their clients into a single memory management model.
  • Code using smart pointers is less explicit about where the resource releases take place.
  • std::unique_ptr expresses ownership transfer using C++11's move semantics, which is relatively new and may confuse some programmers.
  • Shared ownership can be a tempting alternative to careful ownership design, obfuscating the design of a system.
  • Shared ownership requires explicit bookkeeping at run-time, which can be costly.
  • In some cases (e.g. cyclic references), objects with shared ownership may never be deleted.
  • Smart pointers are not perfect substitutes for plain pointers.

If the dynamic allocation is necessary, prefer to keep ownership of the code that allocated it. If other code needs access to the object, consider passing it a copy, or passing a pointer or reference without transferring ownership. Prefer to use std::unique_ptr to make ownership transfer explicit. For example:

std::unique_ptr<Foo> FooFactory();
void FooConsumer(std::unique_ptr<Foo> ptr);

Do not design your code to use shared ownership without a very good reason. One such reason is to avoid expensive copy operations, but you should only do this if the performance benefits are significant, and the underlying object is immutable (i.e.std::shared_ptr<const Foo>). If you do use shared ownership, prefer to use std::shared_ptr.

Never use std::auto_ptr. Instead, use std::unique_ptr.

Source:Google Coding Style

R.1: Manage resources automatically using resource handles and RAII (Resource Acquisition Is Initialization)

Reason

To avoid leaks and the complexity of manual resource management. C++’s language-enforced constructor/destructor symmetry mirrors the symmetry inherent in resource acquire/release function pairs such as fopen/fclose, lock/unlock, andnew/delete. Whenever you deal with a resource that needs to be paired acquire/release function calls, encapsulate that resource in an object that enforces pairing for you – acquire the resource in its constructor, and release it in its destructor.

R.3: A raw pointer (a T*) is non-owning

Reason

There is nothing (in the C++ standard or in most code) to say otherwise and most raw pointers are non-owning. We want owning pointers identified so that we can reliably and efficiently delete the objects pointed to by owning pointers.

A raw reference (a T&) is non-owning

Reason

There is nothing (in the C++ standard or in most code) to say otherwise and most raw references are non-owning. We want owners identified so that we can reliably and efficiently delete the objects pointed to by owning pointers.

R.5: Prefer scoped objects, don’t heap-allocate unnecessarily

Reason

A scoped object is a local object, a global object, or a member. This implies that there is no separate allocation and deallocation cost in excess of that already used for the containing scope or object. The members of a scoped object are themselves scoped and the scoped object’s constructor and destructor manage the members’ lifetimes. Source

Enumerations

We are only using enum class. Enum.3: Prefer class enums over "plain" enums

Prefer using over typedef

Reason

Improved readability: With using, the new name comes first rather than being embedded somewhere in a declaration. Generality: using can be used for template aliases, whereas typedefs can’t easily be templates. Uniformity: using is syntactically similar to auto. T.43: Prefer using over typedef for defining aliases

Example
typedef int (*PFI)(int);   // OK, but convoluted

using PFI2 = int (*)(int);   // OK, preferred

template<typename T>
typedef int (*PFT)(T);      // error

template<typename T>
using PFT2 = int (*)(T);   // OK

Namespaces

We are only using the global namespace eyos. Besides this we are using one level lower namespaces which are expressive and descriptive to the part they are including.

namespace eyos{
    [...]
}

Functions

Output Parameters

The output of a C++ function is naturally provided via a return value and sometimes via output parameters.

Prefer using return values instead of output parameters since they improve readability and oftentimes provide the same or better performance.

Parameters are either input to the function, the output from the function, or both. Input parameters are usually values or const references, while output and input/output parameters will be pointers to none-const.

This is not a hard-and-fast rule. Parameters that are both input and output (often classes/structs) muddy the waters, and, as always, consistency with related functions may require you to bend the rule.

The return type can be auto.

Input Parameters

All variables which are past into the function via lvalue should be marked as const if they are references. In generic functions auto shall be used over strict types. In particular, do not add new parameters to the end of the function just because they are new; place new input-only parameters before the output parameters.

Function overloading

Use overloaded functions (including constructors) only if a reader looking at a call site can get a good idea of what is happening without having to first figure out exactly which overload is being called.

You may write a function that takes a const string& and overload it with another that takes const char*. However, in this case, consider std::string_view instead.

class MyClass {
 public:
  void Analyze(const string &text);
  void Analyze(const char *text, size_t textlen);
};

Default Arguments

Default arguments are allowed on non-virtual functions when the default is guaranteed to always have the same value. Follow the same restrictions as for function overloading, and prefer overloaded functions if the readability gained with default arguments doesn't outweigh the downsides below.

Write Short Functions

Prefer small and focused functions.

We recognize that long functions are sometimes appropriate, so no hard limit is placed on function length. If a function exceeds about 40 lines, think about whether it can be broken up without harming the structure of the program.

Even if your long function works perfectly now, someone modifying it in a few months may add new behavior. This could result in bugs that are hard to find. Keeping your functions short and simply makes it easier for other people to read and modify your code.

You could find long and complicated functions when working with some code. Do not be intimidated by modifying existing code: if working with such a function proves to be difficult, you find that errors are hard to debug, or you want to use a piece of it in several different contexts, consider breaking up the function into smaller and more manageable pieces.

Source: Google Coding Style

Tailing return type

Use trailing return types only where using the ordinary syntax (leading return types) is impractical or much less readable. The trailing return type can make use of arguments in scope (member vars for example)

Examples:

SFINAE The use of the -> guarantee the readability.

template<typename Lhs, typename Rhs>
auto add(const Lhs& lhs, const Rhs& rhs) -> decltype(lhs + rhs) {
    return lhs + rhs;
}

template<typename Lhs, typename Rhs>
decltype(lhs + rhs) add(const Lhs& lhs, const Rhs& rhs) {
    // error: ^^^ 'lhs' and 'rhs' were not declared in this scope
    return lhs + rhs;
}

Research Material

Exceptions and try-catch

We don't use exceptions and try-catch unless the third party library requires them. Use assert instead. (Or our own implementation)

Class definitions

This shows the order and accessibility of methods and fields:

  1. public methods
  2. protected methods
  3. private methods
  4. public fields
  5. protected fields
  6. private fields

The accessibility keywords are not repeated when the accessibility doesn’t change. The only exception is that the accessibility keyword between methods and data is always provided to signal the start of the data block.

Bad example

class Foo
{
    protected: // incorrect: start with public methods
    	virtual void GetDevice() = 0;
    public:
   	 void Print();
    public: // incorrect: don't repeat accessibility
   	 int GetValue();
    private:
 	   void CalculateValue();
    	// incorrect: repeat accessibility for fields
  	  int m_Value;
};

Good Example

class Foo
{
    public:
    	void Print();
    	int GetValue() const;
    protected:
    	void GetDevice() = 0;
    private:
    	void CalculateValue();
    private:
    	int m_Value;
}; 

Using statements

Using statements should be placed on top of the class

Constructors with initializer list

The colon is placed on the newline after the constructor’s signature indented with one tab. The initial field, or base class, is initialized after the colon. Subsequent fields are initialized on their own line, preceded by a colon.

Foo::Foo(const std::string &a_ID) :
		m_ID(a_ID)
        , m_Bar(nullptr)
{
	// ...
}

Initializer list order

The order in which fields are initialized must be the same as they are declared in the class declaration.

Remark: this is required for some compilers or warning levels. Note that the order of initialization is also the same as the order of declaration, so debugging makes a bit more sense because it is done line by line.

Namespaces

namespaces should be snake_case. The reason for this is that our folder structure is also the lower case.

Always use brackets when toggling a Boolean

This will avoid problems when you remove the space between = and ! to make it a whole other operator:

bool b = true;
b = !b; //WRONG

b = (!b); //No room for errors! :D