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 specifyconstexpr
when making a char* constant.
If you omit the
const
keyword, use the eastconst
rule onconstexpr
: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
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
andU
, 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
andatomic 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
};