Autonomous Racing  1
f1tenth Project Group of Technical University Dortmund, Germany
CPP_STYLE_GUIDE

This document is loosely based on the ROS C++ Style Guide and defines a style guide to be followed in writing C++ code in the Autonomous-Racing-PG. This guide SHOULD be followed as close as possible but MUST NOT be seen as holy book that has to be followed without any question.

In case this document does not describe something, try to finde a reasonable solution. In case of discussion: Be excellent to each other.

Terminology

This document uses the following shortcuts:

  • CamelCased: A name is CamelCased if it starts with an uppercase letter, each words starts with an uppercase letter and no special character (e.g. underscore) are between the words e.g. MyNameIsTed.
  • camelCased: A name is camelCased if it starts with a lowercase letter, each words starts with an uppercase letter and no special character (e.g. underscore) are between the words e.g. myNameIsTed.
  • snake_cased: A name is snake_cased if all letters are in lowercase and an underscore is used as a special character between each word e.g. my_name_is_ted.
  • SNAKE_CASED: A name is SNAKE_CASED if all letters are in uppercase and an underscore is used as a special character between each word MY_NAME_IS_TED.

The words SHOULD, SHOULD NOT, MUST, MUST NOT and MAY are used with the semantic described by RFC 2119.

Autoformatting

It is heavily recommended to use the clang-format tool with the provided .clang-format. See the wiki for more information.

Naming

Names SHOULD make it easy for readers to understand your code.

Readers SHOULD need to make as few assumptions and guesses about your code as possible.

Avoid abbreviations and acronyms. Acronyms MAY be used if they can be understood without domain knowledge. Abbreviations MAY be used if their scope is very small.

Packages

A ROS package name SHOULD be snaked_cased.

Topics / Services

A ROS topic and service name SHOULD be snaked_cased.

Files

All filenames SHOULD be snake_cased.

All C++ source files SHOULD have the extension .cpp.

All C++ header files SHOULD have the extension .h and SHOULD be placed in the include directory of the ROS Packages src directory.

If a file mainly contains definition or implementation of a class, the file SHOULD be named snake_cased after the class name e.g. class MyOwnClass results to the filename my_own_class.h/.cpp

A filename SHOULD be descriptive.

You MAY split the implementation on a by-function basis e.g.

include/my/example.h

1 {C++}
2 #pragma once
3 namespace my {
4  class Example {
5  public:
6  int myFunction();
7  int doStuff();
8  };
9 }

src/my/example_my_function.cpp

1 {C++}
2 #pragma once
3 namespace my {
4  int Example::myFunction() {
5  return 0;
6  }
7 }

src/my/example_do_stuff.cpp

1 {C++}
2 #pragma once
3 namespace my {
4  int Example::doStuff() {
5  return 1;
6  }
7 }

Classes

A class name SHOULD be CamelCased e.g. class MyVector. Short acronyms MAY be in all capitals e.g. MyMPC (see MPC) or MyROSNode.

A class name SHOULD NOT be composed of more then three words.

Function

A function name SHOULD be camelCased. Arguments SHOULD be snake_cased. E.g.

1 {C++}
2 int8_t myFunction(std::vector<int8_t>& my_argument, const char* my_argument_2 = nullptr);

As functions usually do something, their name SHOULD describe clearly what they do.

Variables

A variable name SHOULD be snake_cased and SHOULD NOT be cryptic, but try not to make them unnecessary long.

Counter variables MAY only use a single character variable like i, j or k. Iterator variable SHOULD have it in its name. E.g.

1 {C++}
2 std::vector<std::array<uint8_t, 2>>::iterator position_it;
1 {C++}
2 for (size_t i = 0; i < 100; i++)
3 {
4  ROS_DEBUG("%d", i);
5 }

Member variables

All member variable names SHOULD be snake_cased with m_ as prefix. E.g.

1 {C++}
2 class MyClass
3 {
4  private:
5  int64_t m_element_counter;
6  MyClass* m_parent;
7 };

The prefix MAY be omitted if a member variable is public. E.g.

1 {C++}
2 struct MyClass
3 {
4  int64_t element_counter;
5  MyClass* parent;
6 };
1 {C++}
2 class MyClass
3 {
4  public:
5  int64_t element_counter;
6  MyClass* parent;
7 };

Constants

All Constants names SHOULD be SNAKE_CASED.

Try to use constexpr, enum and enum class before resorting to #define or const. E.g.

1 {C++}
2 #define MY_MACRO_CONST 10 // Bad as this macro may be already defined somewhere else
3 const unsigned MY_CONST_CONST = 10; // Better as one would get a compile time error if it was defined somewhere else!
4 constexpr unsigned MY_CONSTEXPR_CONST = 10; // Good as the compiler tries to initialize the variable on compile time if possible.

See Using constexpr to Improve Security, Performance and Encapsulation in C++ for more information on why constexpr is awesome.

ROS Topics / Services

If a ROS topics or services name is stored in a constant, the constant's name SHOULD beginn with TOPIC_.

ROS Parameters

If a ROS parameters name is stored in a constant, the constant's name SHOULD beginn with PARAMETER_.

Global variables

Global variables SHOULD NOT be used. See this.

Global variables names SHOULD be snake_cased with g_as prefix. E.g.

1 {C++}
2 // Reason why this global variable is really necessary
3 my::own::Database g_database;

Namespaces

A Namespace MUST be snake_cased.

Try to avoid long names.

License statements

Each source and header file MAY contain a license and copyright statement.

Formatting

Any C++ code SHOULD be formatted using the provided .clang-format file.

In rare cases where formatting is not wished for, you SHOULD use // clang-format off to disable clang-format. E.g.

1 {C++}
2 // clang-format off
3 int g_magic_numbers[] = {
4  1,2,3,
5  4,5,6,
6  7,8,9,
7  0
8 };
9 // clang-format on

See disabling-formatting-on-a-piece-of-code for more information.

Line length

A line of code SHOULD NOT have more then 120 characters.

Include guard

You SHOULD NOT use #ifndef based include guards, as they are cumbersome and prone for errors. E.g.

1 {C++}
2 // BAD
3 #ifndef MY_HEADER_FILE_H
4 #define MY_HEADER_FILE_H
5 /* CODE */
6 #endif

Instead you SHOULD use #pragma once. E.g.

1 {C++}
2 // GOOD
3 #pragma once
4 /* CODE */

Documentation

Documentation explains the high-level structure of the code. It also provides information on how to use the project to those who didn't read the code.

Code SHOULD be documented in a doxygen compatible fashion.

Function and class summaries SHOULD be omitted if they do not provide more information than the name:

1 {C++}
2 // BAD
3 /**
4  * Returns the size of the list
5  */
6 int List::getSize()
7 {
8  return this->m_size;
9 }

Conversely, classes and functions SHOULD be named so that it is obvious what they do:

1 {C++}
2 // BAD
3 /**
4  * Publishes the Ackermann driving parameters
5  * @param a: The steering angle
6  * @param s: The speed
7  */
8 void send(double a, double s)
1 {C++}
2 // GOOD
3 void publishAckermannDrivingParameters(double steering_angle, double speed)

Comments

Comments SHOULD explain why something is done, not what is being done. They SHOULD explain counter-intuitive semantics, assumptions, invariants and workarounds. However, code that needs explanatory comments can often be avoided by good naming.

1 {C++}
2 // BAD
3 // Increase height by 0.01
4 height += 0.01;
1 {C++}
2 // BETTER
3 // Adjust height to prevent self-collisions
4 height += 0.01;
1 {C++}
2 // GOOD
3 height += NO_SELF_COLLISION_MARGIN;
1 {C++}
2 // BAD
3 // Vector to store length information
4 std::vector<int8_t> vec;
5 
6 // ...
7 
8 // Subtract 1 of each element of the int8_t vector *vec*
9 for (auto& element : vec) {
10  element -= 1;
11 }
1 {C++}
2 // GOOD
3 std::vector<int8_t> lengths;
4 
5 // ...
6 
7 /**
8  * As the vector *lengths* contains length information
9  * and the API for the external lib *my_crappy_lib*
10  * expects not a vector of length information but
11  * a vector of the last valid index, we have to
12  * offset the elements in the vector by -1
13  */
14 for (auto& item_length : lengths) {
15  item_length -= 1;
16 }

Comments SHOULD NOT be used to structure the code. Instead of separating a long function with comments, it SHOULD be split into multiple shorter functions.

Function summary

You SHOULD only add a summary to the declaration of a function.

Text output

You SHOULD NOT use printf or std::cout, instead use rosconsole as it supports:

  • Colored output
  • Verbosity levels
  • Automatically publishing the output to the ROS topic /rosout
  • logging to disk

Macros

You SHOULD NOT use macros. Use constexpr or inline functions if possible, as macros are neither typed nor scoped and prone for errors. E.g.

1 {C++}
2 // BAD
3 #define SQUARE(x) x*x
4 #define DO_WORK(x) x -= 5;
5 // NOT BAD (but also not GOOD)
6 #define SQUARE(x) ((x)*(x))
7 #define DO_WORK(x) do{x -= 5;}while(false);
1 {C++}
2 // GOOD
3 template<typename T>
4 inline constexpr T SQUARE(T x)
5 {
6  return x * x;
7 }
8 template<typename T>
9 inline void DO_WORK(T& x)
10 {
11  x -= 5;
12 }

Preprocessor directives

You SHOULD NOT use preprocessor directives, as they are prone for errors. Use template and inline function as subtitution if possible.

Output arguments

You SHOULD NOT use output arguments, as they obfuscate side effects.

Namespaces

Usage of namespaces is highly encouraged.

The source files MAY be organized by namespace e.g. the file my_class.cpp with the implementation of my::own::MyClass MAY be put in the directory src/my/own/.

The usage of nested namespace (e.g. namespace A::B::C::D::E::F::G::H::J {}) is encouraged.

You SHOULD NOT use a using-directive in a header file. E.g.

1 {C++}
2 // BAD
3 #include <vector>
4 namespace A
5 {
6  using namespace std;
7  vector<int> myFunction();
8 }

Inheritance

You MUST declare an overridden virtual function with the identifiers virtual AND override to clarify whether or not a given function is virtual (and overridden).

1 {C++}
2 // BAD
3 class Base {
4  public:
5  virtual foo();
6  virtual ~Base();
7 };
8 class A : public Base {
9  public:
10  foo() {
11  // Do Stuff
12  }
13  ~A() = default;
14 };
1 {C++}
2 // GOOD
3 class Base {
4  public:
5  virtual foo() = 0;
6  virtual ~Base() = 0;
7 };
8 class A : public Base {
9  public:
10  virtual foo() override {
11  // Do Stuff
12  }
13  virtual ~A() override = default;
14 };

See override and virtual for more information.

If a class is used to define a common interface for several possible implementations, virtual member functions SHOULD be used, as type casting otherwise could lead to hard to debug errors.

It is encouraged to use pure virtual classes as a common interface.

It is encouraged to use virtual only with moderation.

A virtual class SHOULD have a virtual destructor.

Multiple inheritance

It is encouraged to use multiple inheritance only with moderation. Try to avoid it if possible.

Exceptions

Exceptions SHOULD be preferred over the usage of error codes. If you are using error codes, it is highly encouraged to use an enum or enum class as return type.

You SHOULD document what kind of exception a given function might throw. E.g.

1 {C++}
2 /**
3  * @throws IOException Description why and when an IOException is thrown
4  * @throws MyException Description why and when a MyException is thrown
5  */
6 void myFunction() {
7  /* DO STUFF */
8 }

You MUST NOT throw an exception from a destructor.

You MUST NOT throw an exception from a callback function.

If your code can be interrupted by an exception, you MUST make sure such an exception does not lead to an undefined or otherwise broken state e.g. forgetting to free a mutex. This MAY be accomplished by things like a lock_guard.

Enumerations

To prevent conflicts between enums, they SHOULD be either namespaced, classed or declared as a enum class. E.g.

1 {C++}
2 namespace namespaced_enum {
3  enum OptCode {
4  A,
5  B,
6  C
7  };
8 }
9 class classed_enum {
10  public:
11  enum OptCode {
12  A,
13  B,
14  C
15  };
16 }
17 enum class OptCode {
18  A,
19  B,
20  C
21 };

Globals

You SHOULD NOT use global variables and functions, as they create a hidden (global) state and are prone for threading and link time errors. (some more reasons)

Static class variables

You SHOULD NOT use static class member variables, as they create a hidden (global) state and are prone for threading errors.

Magic Numbers

You SHOULD NOT use magic numbers in the source code. E.g.

1 {C++}
2 // BAD
3 int main() {
4  auto response_code = doSomething();
5  if (response_code == 400) {
6  return 1;
7  }
8  return 0;
9 }
1 {C++}
2 // GOOD
3 int main() {
4  auto response_code = doSomething();
5  if (response_code == HTTP_STATUS_CODE::BadRequest) {
6  return EXIT_FAILURE;
7  }
8  return EXIT_SUCCESS;
9 }

If a number has a special meaning, usage of an constexpr, enum/enum class to address it is strongly recommended.

You SHOULD NOT replace a magic number with a magic constant. E.g.

1 {C++}
2 // BAD
3 int main() {
4  auto response_code = doSomething();
5  if (response_code == OTHER_NUMBERS::FOUR_HUNDRED) {
6  return NUMBER::ONE;
7  }
8  return NUMBER::ZERO;
9 }

Assertions

The usage of assertions to check invariants and assumptions is highly encouraged.

You SHOULD use the ROS assertions (ROS_COMPILE_ASSERT(cond),ROS_STATIC_ASSERT(cond),ROS_ASSERT(cond),ROS_ASSERT_MSG(cond, "format string", ...),ROS_ASSERT_CMD(cond, function())) provided in ros/assert.h. E.g.

1 {C++}
2 int64_t divide(int64_t a, int64_t b) {
3  ROS_ASSERT(b != 0);
4  ROS_ASSERT_MSG(b != 0, "Division by zero");
5  ROS_ASSERT_CMD(b != 0, callRufus());
6  return a / b;
7 }

See http://docs.ros.org for more information.

You SHOULD NOT do work in an assertion. E.g.

1 {C++}
2 // BAD
3 int64_t sum(int64_t a, int64_t b) {
4  ROS_ASSERT_MSG((a + b) = 0, "If you divide the number %d by %d you get %d", a, b, divide(a,b));
5  return a + b;
6 }

Deprecation

You SHOULD use the deprecated attribute to declare a struct, class, enum, function or other elements as deprecated. E.g.

1 {C++}
2 namespace [[deprecated]] my_namespace {
3  class MyClass;
4 }

You SHOULD use #pragma GCC warning "MSG" or #pragma GCC error "MSG" do deprecate whole files. E.g.

1 {C++}
2 #pragma GCC warning "Files was moved to another location"
3 #include <new/location/filename.h>

or

1 {C++}
2 #pragma GCC error "Class MyClass was removed in Version 2.0"
3 // Here was once a class called MyClass

Main function

The main function of a program SHOULD be in a seperate .c/.cpp file.

Zero cost abstraction

Zero cost abstractions like std::array or std::lock_guard SHOULD always be prefered.

1 {C++}
2 // BAD
3 int64_t arr[3] = {1,2,3};
4 // BETTER
5 auto arr = std::array<int64_t, 3>({1,2,3});
6 // OR
7 std::array<int64_t, 3> arr = {1,2,3};
8 // GOOD (but experimental)
9 auto arr = std::experimental::make_array(1,2,3);
10 // See: https://en.cppreference.com/w/cpp/experimental/make_array
11 
12 //BAD
13 void foo() {
14  resource_mutex.lock();
15  // DO STUFF
16  resource_mutex.unlock();
17 }
18 // GOOD
19 void foo() {
20  {
21  auto lock_guard = std::lock_guard<std::mutex>(resource_mutex)
22  // DO STUFF
23  }
24 }