Good Practices

This section covers strongly recommended practices, that, will not only enable for consistent project conventions, but also ease the life of the programmer, when remembering the names of methods or members a of class.

CRUD

CRUD stands for Create Read Update and Delete. These operations are commonly used for a variety of applications in C++ classes. Here are defined the conventions to a more uniform methodology of naming the methods of your classes.

Consider the example below:

#include <iostream>
#include <memory>
#include <utility>
#include <vector>

namespace hohenheim
{

template<typename T>
using Data = std::vector<T>;

template<typename T>
using const_iterator = typename Data<T>::const_iterator;

template<typename T>
using Storage = std::shared_ptr<Data<T>>;

template<typename T>
class MyClass
{
  private:
  // Private members
    Storage<T> data;
  public:
  // Constructors
    template<typename U>
    MyClass(U&& u) noexcept;
  // Iterators
    const_iterator<T> cbegin() const noexcept;
    const_iterator<T> cend() const noexcept;
  // Public Methods
    // Modifiers
    T const& at() const noexcept;
    template<typename U>
    void push_back(U&& u) noexcept;
    template<typename RandIt>
    void erase(RandIt&& it) noexcept;
};

} // namespace hohenheim

   For your class methods, use names in accordance with the standard library, in this example, the operations used were base on the ones implemented for the std::vector container.

   How will this help me in the long run?

Consider that the software you are developing is composed of a large amount of classes; for each of them, you define distinct names for CRUD operations, for example, remove for one, delete for the other, and erase for a third one. You will have to constantly check the class file or the documentation to know which method is the one you are looking for. Choosing erase for all makes it transparent what you should write to perform the action of erasing an item.

East const

The const Keyword

Follow the east const convention, which be better explained by an example, lets start with the int32_t and char keywords.

int32_t const var{};           // East const
const int32_t var{};           // West const
char const * string {"Hello"}; // East const
const char * string {"Hello"}; // West const

The first and second lines can be read, from the right to the left as follows:

An identifier to a constant integer of 32 bits.

A identifier to a pointer to a constant char

Taking it a step further, it is possible to see the consistency when reading code using convention, consider the snippet below:

char const * const string {"Hello"};

The from the right to the left it reads as follows:

An identifier to a constant pointer to a constant char.

The constexpr Keyword

For the constexpr keyword, keep it always in the begging of the statement.

constexpr int32_t const   var       {42};
constexpr char    const * my_string {"Hello"};

To put it into words, the first and second lines would translate to:

A identifier that is a constant integer of 32 bits.

A identifier that is a pointer to a constant char.

   constexpr const   Some compilers will give a warning if you just specify constexpr when making a char* constant.

   If you omit the const keyword, use the east const rule on constexpr:

constexpr int32_t var {42};

The static keyword

For the static keyword, use it always at the beggining of your variable definition or declaration ( in the case where the definition of the static variable is elsewhere ).

static constexpr int32_t const         var       {42};
static constexpr char    const * const my_string {"Hello"};

The lines in the snippet above can be read as follows:

An identifier to a constant integer of 32 bits which is static.

An identifier to a constant pointer to a constant char which is static.

Storages

Inspired by the EPFL Logic Synthesis Libraries it is recommended to use a Storage alias to instantiate a data item inside your class.

How does it work?

Taking advantage of using declarations you can increase the readability of your code with simple aliases, below is sample snippet of a generic graph class using this convention:

namespace graph {

// First alias to define nodes
template<typename T>
using Nodes = std::multimap<T,T>;
// Second one to put them into dynamic memory
template<typename T>
using Storage = std::unique_ptr<Nodes<T>>;

template<typename T>
class Graph
{
  private:
  // Private Members
    Storage<T> graph; // Simple and concise.
  public:
  // Constructors
    Graph();
    Graph(std::initializer_list<std::pair<T,T>> t);
  // Public Methods
    // Element Access
    template<typename U>
    std::vector<T> get_adjacent(U&& u);
    // Modifiers
    template<typename U>
    void emplace(U&& u);
    template<typename U, typename... Args>
    void emplace(U&& u, Args&&... args);
};

} // namespace graph

   What if I have more than one data item in my class?

Replace storage with meaningful names, preferably generic if using templates. Given a generic Person class:

...
 Name<T> name; // Could be a struct with first, middle, last; or a string
 Address<T> address; // Could be a struct with the coherent fields.
...

Comments

Inline comments

Should be of the form //.

Example:

uint32_t work_hours; // Includes extra hours

Multiline comments

Should be of the same form mentioned before, expanded throughout as many lines as necessary.

Example:

//
// Quick-sort implementation.
// Uses iterators from the begining and end
// of the container.
//

   Notice the leading and the trailing // above, its recommended to use them so they better separate the code from your comment.

   DO NOT, use /* */, inline or multiline comments.

Namespaces

   The code inside a namespace should be in the same nesting level as the namespace.

Example

#include <iostream>
#include <cstdint>
#include <utility>
#include <string>
#include <memory>

namespace person
{

template<typename T>
using Name = std::unique_ptr<T>;
template<typename T>
using Age = std::unique_ptr<T>;

template<typename T1, typename T2>
class Person
{
  private:
  // Private Members
    Name<T1> name;
    Age<T2> age;
  public:
  // Constructors
    template<typename U1, typename U2>
    Person(U1&& name, U2&& age);
  // Public Methods
    // Element Access
    T1 const& get_name();
    T2 const& get_age();
  // Operators
    template<typename _T1, typename _T2>
    friend std::ostream& operator<<(std::ostream& os, Person<_T1,_T2> const& p);
};

//
// Constructors
//

template<typename T1, typename T2>
template<typename U1, typename U2>
Person<T1,T2>::Person(U1&& name, U2&& age)
  : name(std::make_unique<
      typename Name<T1>::element_type>(std::forward<U1>(name)))
  , age(std::make_unique<
      typename Age<T2>::element_type>(std::forward<U2>(age)))
{
}

//
// Public Methods
//

template<typename T1, typename T2>
T1 const& Person<T1,T2>::get_name()
{
  return *(this->name);
}

template<typename T1, typename T2>
T2 const& Person<T1,T2>::get_age()
{
  return *(this->age);
}

//
// Operators
//

template<typename _T1, typename _T2>
std::ostream& operator<<(std::ostream& os, Person<_T1,_T2> const& p)
{
  std::cout << "Name: " << *(p.name) << std::endl;
  std::cout << "Age: " << *(p.age);
  return os;
}
} // namespace person

int main(int argc, char const* argv[])
{
  using Person = person::Person<std::string, int16_t>;

  std::string name_augustine = "augustine";
  Person augustine{name_augustine, 33};

  Person mary{"mary", 42};

  std::cout << mary << std::endl;
  std::cout << augustine << std::endl;

  return 0;
}

Structuring your namespaces

   All the namespaces names should be in the singular form.

Consider the following project structure:

- anastasia
| - include
| | - CMakeLists.txt
| | - anastasia
| | | - component-a.hpp
| | | - component-a
| | | | - algorithm.hpp
| | | | - enumeration.hpp
| | | - component-b.hpp
| | | - component-b
| | | | - binary-tree.hpp
| | | | - data-structure.hpp
| - extern
| - doc
| - test
| - CMakeLists.txt

The approach taken to minimize name collision, is to follow the directory structure of your project.

   Now, how does that work?

The top level file

Lets consider the file component-a.hpp, it is in the top level of the anastasia project source files. Therefore, it should use the namespace equivalent to its name in the snake_case format.

Example

The example below illustrates the contents of the component-a.hpp file.

namespace component_a
{
// Functionalities
} // namespace component_a

   Reiterating, the namespace is the source filename converted, to the snake_case format

   Does the namespace name has to be grammatically equivalent to the class name?

That is true for the top level source file. This simplifies the access to a functionality, given a namespace, you know how to access it in a more natural way.

   What about the components/functionalities of your class?

This will the discussed below.

The components/functionalities

Now lets consider one of the source files of the component-a folder.

   The component-a folder represents the implementations of component-a.hpp functionalities/components.

The methodology for naming the namespace is the same as of the top level source file, i.e., use the filename. Only this time, you are representing a functionality/component of your top level source file, so it should be nested within the namespace of the top level source file.

Example

Give the component-a/algorithm.hpp file, the namespace should be structured as follows:

namespace component_a::algorithm
{
} // namespace component_a::algorithm

   Notice that, in both examples, the namespace ends with a comment that uses the namespace keyword, and follows by repeating the namespace identifier. This is a good practice to avoid accidentally erasing the namespace closing bracket.

Scopes

Scope Delimiters { and } should be below the target keywords.

Example:

namespace anastasia
{
  // content
} // namespace anastasia

Argument List delimiters ( and ), ( should be concatenated with the keyword.

Example:

if( my_bool == true )
{
  // do some logic
}

Every operator in the argument list has a single space between operands.

The initial and final operands should have a single space from the parenthesis.

Example:

if( my_bool == true && other_bool == false )
{
  // do some logic
}

Primitives

Integral Types

Integral types should be used from the <cstdint> header, those which consist of:

  • [u] int8_t
  • [u] int16_t
  • [u] int32_t
  • [u] int64_t

Examples:

#include <cstdint>

int main(int argc, char const* argv[])
{
  uint16_t counter;
  uint64_t grid_size;
  return EXIT_SUCCESS;
}

Floating-Point Types:

Floating-Point Types should the alises below:

  • float32_t
  • float64_t

Examples

typedef float float32_t;
typedef double float64_t;

int main(int argc, char const* argv[])
{
  float32_t pi{3.14};
  float64_t res{8.77443};
  return EXIT_SUCCESS;
}

References

cstdint

fundamental-types

Identifiers

Identifiers should be written in snake_case.

Examples:

int32_t my_variable{};
uint64_t total_cost{};
float64_t score{};

Enumerations

Enumerations definitions are written in CamelCase. Preferably, use scoped enumerations, below is an adapted example from the cppreference website.

Example

enum class Altitude: char
{ 
  HIGH='h',
  LOW='l', // C++11 allows the extra comma
}; 

The enumerators names are written in UPPERCASE.

Example

enum class Direction
{
  LEFT,
  RIGHT,
  UP,
  DOWN,
};

Classes

Naming

Class names are written in CamelCase.

Examples:

class CamelCase
{
  MyClass() = default;
}
class MyInterface
{
  insert() = 0;
}

Member functions

The member functions of a class are written in snake_case.

Friends and operators of a class are also included.

   For the naming of your classes methods, choose names based on containers or algorithms from the C++ standard library, this way, it is easier to remember the name of a method to execute an action, since all follow the same pattern.

   What if the name is not on the list? For instance, I want to create a person class, which has a getter and a setter for a name and age.

In this case, use the name of the function prepended with the get or set. For example:

void set_name(std::string name);
std::string get_name();
int16_t get_age();
void set_age(int16_t age);

   What if I am dealing with a class that has multiple members that use a common method, such as erase or find?

In this case, use the member as the suffix for the method.

 int16_t find_id( std::string name );
 int16_t find_address( in16_t id );

   Even if you only implement a getter or setter for piece of data in a class, you should still preprend the get_ or set_.

Examples:

class CamelCase
{
  public:
    // Constructors
      CamelCase() = default;
    // Public Methods
      // Element Access
      int find();
      // Modifiers
      void insert();
      void erase();
    // Operators
      friend std::ostream& operator<<(ostream& os, CamelCase rhs);
}

Comment your class sections

Visual aid is always nice to quickly find what you are looking for.

Example:

template<typename T>
using Name = std::unique_ptr<T>;
template<typename T>
using Age = std::unique_ptr<T>;

template<typename T1, typename T2>
class Person
{
  private:
  // Private Members
    Name<T1> name;
    Age<T2> age;
  public:
  // Constructors
    template<typename U1, typename U2>
    Person(U1&& name, U2&& age);
  // Public Methods
    // Element Access
    T1 const& get_name();
    T2 const& get_age();
  // Operators
    template<typename _T1, typename _T2>
    friend std::ostream& operator<<(std::ostream& os, Person<_T1,_T2> const& p);
};

   There is a model to start with in the resources section.

   Inside the class and in the methods definitions, do not include empty commented sections, you can always consult offered model.

   Notice how the commented sections inside the class, are on the same level as the access specifiers.

   Notice how the member types are nested on the same level as the method, i.e., // Element Access, do not include these in the definitions of your methods.

Templates

This section describes the used conventions when using templates with classes, algorithms and iterators.

Classes and Functions

Templated classes and functions

Identifiers should be written in CamelCase.

   For one template parameter use T or a descriptive name, e,g:.

template<typename T>
T sum( T&& a, T&& b )
{
  return a+b;
}

or

template<typename Integral>
T sum( Integral&& a, Integral&& b )
{
  return a+b;
}

   For two template parameters, use T and U, or a descriptive name.

template<typename T, typename U>
auto sum_first( T&& a, U&& b )
{
  return a.first+b.first;
}

or

template<typename Pair1, typename Pair2>
auto sum_first( Pair1&& a, Pair2&& b )
{
  return a.first+b.first;
}

   For more than 2 template parameters, use T1 ... Tn or a descriptive name.

template<typename T1, typename T2, typename T3>
bool all_equal( T1&& a, T2&& b, T3&& c)
{
  return (a == b) && (b == c);
}

Concepts

Concepts

Identifiers should be written in CamelCase.

template<typename T, typename U>
concept ConvertibleTo = std::convertible_to<std::decay_t<T>,std::decay_t<U>>;

Requirements

Do not indent multiline requires clauses.

template<typename T>
concept Referenceable =
requires
{
  typename Reference<T>;
  typename ConstReference<T>;
};

   For conjunctions, disjunctions and atomic constrains follow the models below:

Do not use traits directly, alias them as concepts

template<typename T>
concept Integral = std::is_integral_v<T>;

template<typename T>
concept Floating = std::is_floating_point_v<T>;

One line disjunctions (can be also applied to conjunctions)

template<typename T>
concept Arithmetic = Integral<T> || Floating<T>;

Multiline disjunctions (can be also applied to conjunctions)

template<typename T>
concept Arithmetic =
Integral<T>
|| Floating<T>
|| requires(T t)
  {
    { t+t } -> std::same_as<T>;
    { t-t } -> std::same_as<T>;
    { t*t } -> std::same_as<T>;
    { t/t } -> std::same_as<T>;
  };

   One level of indentation is allowed in this scenario.

Parameter packs with fold expressions

// Is U pairs of a type T?
template<typename T, typename... U>
concept IsPairsOf =
requires(U... u)
{
  { ((u.first),...) } -> std::convertible_to<std::decay_t<T>>;
  { ((u.second),...) } -> std::convertible_to<std::decay_t<T>>;
};

Iterators

Iterators should be written as follows:

  • For input iterators, use:
template<typename InIt>
  • For output iterators, use:
template<typename OutIt>
  • For forward iterators, use:
template<typename FwdIt>
  • For bidirectional iterators, use:
template<typename BidIt>
  • For random access iterators, use:
template<typename RandIt>

Directories

Naming

Files and directories should be named using lower-case words separated using a dash '-'.

This rule does not apply to a CMakeLists file.

Examples:

- my-project
| - include
| | - CMakeLists.txt
| | - heianhouer
| | | - heianhouer.hpp
| - extern
| - doc
| - CMakeLists.txt

Recommended Project Structure

Given a project named anastasia, the recommended project structure is shown below:

- anastasia
| - include
| | - CMakeLists.txt
| | - anastasia
| | | - component-a.hpp
| | | - component-a
| | | | - algorithm.hpp
| | | | - enumeration.hpp
| | | - component-b.hpp
| | | - component-b
| | | | - binary-tree.hpp
| | | | - data-structure.hpp
| - extern
| - doc
| - test
| - CMakeLists.txt

Where:

   The root folder has to match the project's name.

   An include folder has all the source files.

   Each component has a header to its functionalities.

Each of these functionalities, those needing algorithms or data structures, are stored in a directory with the same name of the component header.

   An extern folder must contain all the external libraries.

   A test folder, should be used for testing modules and algorithms.

   The test folder, should always, be used in the singular.

   A doc folder is used for all the project's documentation.

Resources

This section presents models to use during your project and ease the consistent implementation of your classes, methods and algorithms.

Class

Templated Class:

template<typename T>
class Storage
{
  private:
  // Private members
  public:
  // Public Members
  public:
  // Constructors
  // Iterators
  // Public Methods
    // Element Access
    // Capacity
    // Modifiers
    // Lookup
    // Operations
    // Observers
  private:
  // Private Methods
};

Non-templated Class

class Storage
{
  private:
  // Private members
  public:
  // Public Members
  public:
  // Constructors
  // Iterators
  // Public Methods
    // Element Access
    // Capacity
    // Modifiers
    // Lookup
    // Operations
    // Observers
  private:
  // Private Methods
};