C++ Style Guide

From Spire Trading Inc.
Revision as of 13:47, 15 April 2026 by Kamal (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

This article specifies the most general guidelines used for all Spire projects written using C++ as the primary programming language. Each individual project may extend or override the guidelines specified herein. The overall aim of this guideline is to provide consistency across the numerous projects developed by Spire in order to promote and benefit from modern C++ practices as well as tailor them to our specific requirements.

In general, the guidelines are very specific and opinionated. This is done intentionally to ensure that every detail has been given proper consideration and that every line of code is written with care and diligence. It is taken as a prerequisite that writing robust, clean and maintainable code requires paying close attention to even the smallest of details. As such, when there are two or more ways to write or express any given statement or expression, be it using spaces or tabs, number of characters per line, purple or violet, the guideline will often require that one particular approach be used to the exclusion of others.

Finally, this document is constantly evolving and as such older projects may not be up-to-date with the guidelines found here. In those circumstances one must use their best judgment about how to integrate these guidelines into older codebases. As a general principle, consistency should favor the local over the global, that is it is more important to be consistent with a function definition than within a file, and more important to be consistent within a file than within a directory, project, etc...

Core Guidelines

This document should be seen as an extension to the Cpp Core Guidelines authored and maintained by Herb Sutter and Bjarne Stroustrup. After reviewing these guidelines one should then become familiar with the Cpp Core Guidelines as documented here:

https://github.com/isocpp/CppCoreGuidelines

Those guidelines specify the best practices for writing modern C++ code. In situations where there is a conflict between the Cpp Core Guidelines and the guidelines set forth in this document, this document takes precedence. For all known situations where a conflict exists, this document should explicitly indicate that conflict to avoid confusion.

Development Environment

All projects target the C++23 standard. The two primary development environments supported by Spire projects are Linux and Windows. CMake is used to produce the appropriate project/make files for each platform.

For source control, git is used and projects are to be hosted on Spire Trading's Github page.

Each developer is welcome to use an editor of their choice.

File Names

Use CamelCase for header and source file names, matching the primary type or class defined within. The main exception is for CMake files which must use the name CMakeLists.txt. Directory names use snake case with all lower case letters.

Header files should use the extension hpp

Source files should use the extension cpp

All files end with a single new line character.

Directory Structure

A project is broken down into one library component and one or more application components. The library is written in a header-only fashion, that is the implementation is provided directly in the header file rather than a separate .cpp source file. This allows for such libraries to easily be incorporated into other projects without the need for complex build configurations. Applications often use a mix of header only files and source files.

At the root of a project are the build scripts: build.bat/build.sh to build the project, configure.bat/configure.sh to produce the platform specific build files (VS solutions for Windows, make files for POSIX), and setup.bat/setup.sh to download all third party dependencies. Some projects may also include install scripts, version scripts, and scripts for continuous builds or deployment.

The project directory contains:

  • Include - Header files (.hpp), organized by component under a project-level namespace directory.
  • Source - Unit test source files (.cpp), organized into ComponentTests directories.
  • Config - CMake build configuration files, with a subdirectory per component.
  • Dependencies - Third party dependencies downloaded by the setup script.

Applications go into a separate Applications directory at the project root. Each application follows the same structure with its own Include, Source, Config, and Dependencies directories alongside its own build scripts.

The following example shows the structure of a project named TicTacToe consisting of a library with two components (Board and Rules), and two applications (TicTacToeConsole and TicTacToeUi):

TicTacToe                          # Root level directory.
  build.bat                        # Builds all libraries and applications.
  build.sh
  configure.bat                    # Produces platform specific build files.
  configure.sh
  setup.bat                        # Downloads all third party dependencies.
  setup.sh
  \Applications                    # Contains all applications.
    \TicTacToeConsole              # A console application.
      build.bat                    # Application build scripts.
      build.sh
      configure.bat
      configure.sh
      CMakeLists.txt               # Top-level CMake config for the application.
      \Config                      # CMake build files per component.
        \TicTacToeConsole          # CMake config for the main executable.
          CMakeLists.txt
      \Include                     # Application header files.
        \TicTacToeConsole          # Namespace directory.
          ConsoleRenderer.hpp
      \Source                      # Application source files.
        \TicTacToeConsole          # Source files for the application.
          ConsoleRenderer.cpp
          main.cpp
    \TicTacToeUi                   # A GUI application.
      \...                         # Same structure as TicTacToeConsole.
  \TicTacToe                       # The main library.
    build.bat                      # Library build scripts.
    build.sh
    configure.bat
    configure.sh
    setup.bat                      # Downloads library dependencies.
    setup.sh
    CMakeLists.txt                 # Top-level CMake config for the library.
    \Config                        # CMake build files per component.
      \Board                       # CMake config for the Board component.
        CMakeLists.txt
      \Rules                       # CMake config for the Rules component.
        CMakeLists.txt
    \Include                       # Library header files.
      \TicTacToe                   # Namespace directory.
        \Board                     # Headers for the Board component.
          Board.hpp
          Cell.hpp
        \BoardTests                # Test utility headers.
          TestBoard.hpp
        \Rules                     # Headers for the Rules component.
          MoveValidator.hpp
          WinChecker.hpp
    \Source                        # Library source and test files.
      \BoardTests                  # Unit tests for the Board component.
        BoardTester.cpp
        CellTester.cpp
        main.cpp
      \RulesTests                  # Unit tests for the Rules component.
        MoveValidatorTester.cpp
        WinCheckerTester.cpp
        main.cpp
    \Dependencies                  # Third party dependencies.

Header File Structure

Include Guard

Header files are structured first with an include guard defined using SNAKE_CASE whose name consists of the project name appended to the directory name and finally the file name itself, ending with a _HPP suffix. As a note, all files end with a single new line character.

1 #ifndef CPP_CHAT_CHAT_SERVER_HPP
2 #define CPP_CHAT_CHAT_SERVER_HPP
3   ...
4 #endif

Include Directives

Next come the list of #include directives. Include files are ordered based on their category where the first category is standard C++ header files, the second category is external dependency header files, and finally the third category is project header files. Both standard and external dependency header files use angle brackets (#include <...>) and local project header files use quotes (#include "..."). Within each category files are listed in alphabetical order.

1 #include <tuple>
2 #include <vector>
3 #include <boost/noncopyable.hpp>
4 #include "cpp_chat/cpp_chat.hpp"
5 #include "cpp_chat/definitions.hpp"

Namespaces

After include directives the project's namespace is defined. All declarations and definitions must be contained within a namespace so as to avoid polluting the global namespace. Namespace definitions should be preceded by one single new line except when immediately following a namespace definition. The top level namespace is named after the project, and sub-namespaces may be used (although they should be used very sparingly). When possible, use the nested namespace syntax (A::B) instead of separate nested declarations.

 1 // Correct, uses nested namespace syntax.
 2 namespace CppChat::Details {
 3   ...
 4 }
 5 
 6 namespace CppChat {
 7   ...
 8 }
 9 
10 namespace CppChatExtra {
11   ...
12 }
13 
14 // Incorrect, uses separate nested declarations.
15 namespace CppChat {
16 namespace Details {
17   ...
18 }
19 }

One situation where nested namespaces are welcome and encouraged are for implementation details that do not form part of the namespace's public interface. These definitions are put in a nested namespace called Details and go into a file of their own whose name contains the suffix details. For example if the cpp_chat_server.hpp contains implementation details, then they should be included in the file cpp_chat_server_details.hpp and the definitions should go into the namespace CppChat::Details.

Layout

Indentation

Code is indented using 2 spaces per level, tabs are not permitted.

1 int factorial(int n) {
2   if(n == 0) {
3     return 1;
4   }
5   return n * factorial(n - 1);
6 }

Line Structure

Lines are limited to 80 characters. Exceptions to this rule are long include files and long string literals where breaking up the literal into multiple lines is not possible. In order to break up long statements into multiple lines, break the line after the earliest operator or punctuation mark (which includes commas, operators, and opening brackets).

 1 // Correct, line break at comma.
 2 f(a, ..., b,
 3   c, ..., d);
 4 
 5 // Incorrect, line break after expression.
 6 f(a, ..., b
 7   , c, ..., d);
 8 
 9 // Correct, line break after operator.
10 a + ... + b +
11   c + ... + d;
12 
13 // Incorrect, line break after expression.
14 a + ... + b
15   + c + ... + d;
16 
17 // Correct, line break after opening bracket.
18 f(
19   a, ..., b, c, ..., d);
20 
21 // Incorrect, line break after function name.
22 f
23   (a, ..., b, c, ..., d);
  • For statements that begin new blocks of code (such as if/while/class...), in order to avoid confusing the line continuation with the code block, the line continuation is indented two extra levels. For example consider the following snippet of code:
1 // Incorrect.
2 if(condition_a && condition_b && ... &&
3   condition_c) {
4   std::cout << "meow" << std::endl;
5 }

The line continuation on line 2 clashes with the code block on line 3. To avoid this clash the above code is indented as follows:

1 // Correct.
2 if(condition_a && condition_b && ... &&
3     condition_c) {
4   std::cout << "meow" << std::endl;
5 }

The extra level of indentation in the line continuation makes it clear where the condition ends and the code block begins.

Declaration Spacing

Documented declarations shall be separated from the following declaration by a blank line. Any two non-documented declarations shall not have a blank line between them. In general, public declarations shall be documented. This rule applies to all declarations including class members, free functions, and free variables.

Braces

Braces are placed using a variant of the OTBS style. The opening brace is placed on the same line as the declaring statement with one single space preceding it, and the closing brace is placed on a line of its own at the same level of indentation as the declaring statement. For if statements, the else/else if is placed on the same line as the closing brace. For do/while loops, the while is placed on the same line as the closing brace.

Examples:

 1 // Correct.
 2 if(<cond>) {
 3   <body>
 4 } else {
 5   <default>
 6 }
 7 
 8 // Correct.
 9 do {
10   <body>
11 } while(<cond>);
12 
13 // Incorrect.
14 if(<cond>)
15 {
16   <body>
17 }
18 else
19 {
20   <default>
21 }
22 
23 // Incorrect.
24 do
25 {
26   <body>
27 }
28 while(<cond>);

Spacing

The following are correct use cases for white spaces:

  • Used for indentation.
  • Used to surround binary operations.
1 // Correct.
2 auto x = a + b;
3 auto y = 5;
4 
5 //! Incorrect
6 auto x = a+b;
7 auto y=5;
8 auto z= 5;
  • One space is placed after a comma.
1 // Correct.
2 f(1, 2);
3 int g(int a, int b);
4 
5 //! Incorrect
6 f(1,2);
7 int g(int a,int b);

Syntax

Function Definitions

Functions declared and defined in header files are formatted as follows:

 1 inline int f() {
 2   ...
 3   return 123;
 4 }
 5 
 6 template<typename T>
 7 bool g() {
 8   ...
 9   return false;
10 }

That is they are declared as inline unless they are function templates in which case the inline specified is omitted.

Variable Declarations

Variables are declared so that only one variable is declared per line, the type of the variable should almost always use auto, and variables should almost always be initialized.

Examples of simple declarations:

1 auto x = 5;
2 auto y = a + b;

For complex initialization of values, use an immediately invoked lambda expression as follows:

1 auto value = [&] {
2   if(condition) {
3     return 123;
4   }
5   return 321;
6 }();

Class Declaration Layout

Within each access section (public, protected, private), declarations are ordered as follows:

  1. Nested types (enums, nested classes, typedefs, type aliases)
  2. Static member variables
  3. Static member functions
  4. Default constructor
  5. Implicit constructors (compiler may call implicitly, e.g. non-explicit single parameter)
  6. Explicit single parameter constructors
  7. Multi-parameter constructors (non-copy, non-move)
  8. Copy constructor
  9. Move constructor
  10. Destructor
  11. Conversion operators
  12. New (non-inherited) methods
  13. Inherited/overridden methods
  14. Arithmetic operators
  15. Comparison operators
  16. Assignment operators

Within a class, there must always be a single blank line between the destructor and the following declaration in the same access section. If there is no destructor, then a single blank line between the last constructor and the following declaration in the same access section.

 1 /** Base class for types that can be converted to a string. */
 2 class Printable {
 3   public:
 4     virtual ~Printable() = default;
 5 
 6     /** Returns a string representation. */
 7     virtual std::string to_string() const = 0;
 8 };
 9 
10 /** Represents a time duration. */
11 class Duration : public Printable {
12   public:
13 
14     /** Enumerates time units. */
15     enum class Unit {
16       SECONDS,
17       MILLISECONDS
18     };
19 
20     /** The type used to store the count. */
21     using Count = std::int64_t;
22 
23     /** The zero duration constant. */
24     static const Duration ZERO;
25 
26     /** Returns a Duration from a count of milliseconds. */
27     static Duration from_milliseconds(Count milliseconds);
28 
29     /** Constructs a zero Duration. */
30     Duration();
31 
32     /** Implicitly converts a count of seconds. */
33     Duration(Count seconds);
34 
35     /** Constructs a Duration from a string representation. */
36     explicit Duration(std::string representation);
37 
38     /** Constructs a Duration from a count and unit. */
39     Duration(Count count, Unit unit);
40 
41     Duration(const Duration& other);
42     Duration(Duration&& other) noexcept;
43     ~Duration() override;
44 
45     /** Returns true if this is a non-zero duration. */
46     explicit operator bool() const;
47 
48     /** Returns the count in the given unit. */
49     Count get_count(Unit unit) const;
50 
51     std::string to_string() const override;
52 
53     Duration operator +(const Duration& rhs) const;
54     Duration operator -(const Duration& rhs) const;
55     bool operator ==(const Duration& rhs) const;
56     auto operator <=>(const Duration& rhs) const;
57     Duration& operator =(const Duration& rhs);
58     Duration& operator =(Duration&& rhs) noexcept;
59     Duration& operator +=(const Duration& rhs);
60     Duration& operator -=(const Duration& rhs);
61 
62   private:
63     Count m_milliseconds;
64 };

Note how the copy constructor, move constructor, and destructor are undocumented and grouped without blank lines. The overridden to_string appears after the new method get_count. Arithmetic, comparison, and assignment operators each form their own group at the end with no blank lines between them.

Naming Conventions

Types

Use CamelCase for all type names including classes, structs, enums, type aliases, typedefs, and concepts.

1 class ChatServer { ... };
2 enum class ConnectionStatus { ... };
3 using MessageList = std::vector<Message>;
4 template<typename T>
5 concept Serializable = ...;

Functions and Variables

Use snake_case for function and variable names.

1 auto message_count = get_message_count();
2 void send_message(const Message& message);

Type Traits

Type traits use snake_case to be consistent with the standard library convention.

1 template<typename T>
2 struct is_serializable { ... };

Class Fields

Class member fields are prefixed with m_.

1 class ChatServer {
2   private:
3     std::string m_name;
4     int m_port;
5 };

General Naming Rules

Do not use abbreviations for names. Variable names should be a single noun, dropping adjectives unless needed to disambiguate.

 1 // Correct.
 2 auto connection = make_connection();
 3 auto server = find_server();
 4 
 5 // Incorrect, uses abbreviations.
 6 auto conn = make_connection();
 7 auto srv = find_server();
 8 
 9 // Correct, adjective needed to disambiguate.
10 auto source_connection = get_source();
11 auto destination_connection = get_destination();

Constructors

Explicit Constructors

Single parameter constructors shall be declared explicit to prevent implicit conversions.

1 class Port {
2   public:
3 
4     // Correct.
5     explicit Port(int value);
6 
7     // Incorrect, allows implicit conversion from int.
8     Port(int value);
9 };

Noexcept

Constructors and assignment operators shall be declared noexcept if their implementations are actually noexcept. There is no need for noexcept on other methods.

1 class Connection {
2   public:
3     Connection(Connection&& other) noexcept;
4     Connection& operator =(Connection&& other) noexcept;
5 
6     // No noexcept needed on regular methods.
7     void send(const Message& message);
8 };

Initialization

Objects are always initialized using parentheses, never braces. The exception is when constructing a std::initializer_list.

 1 // Correct.
 2 auto server = ChatServer("localhost", 8080);
 3 auto value = std::optional<int>(5);
 4 
 5 // Incorrect, uses braces.
 6 auto server = ChatServer{"localhost", 8080};
 7 auto value = std::optional<int>{5};
 8 
 9 // Exception: initializer_list.
10 auto values = std::vector<int>({1, 2, 3});

Template Argument Deduction

Use compile time argument deduction (CTAD) as much as possible. Provide deduction guides for all class templates unless a compiler generated deduction guide is available.

 1 template<typename T>
 2 class Wrapper {
 3   public:
 4     explicit Wrapper(T value);
 5 
 6   private:
 7     T m_value;
 8 };
 9 
10 // Deduction guide.
11 template<typename T>
12 Wrapper(T) -> Wrapper<T>;
13 
14 // CTAD usage.
15 auto wrapper = Wrapper(42);

Documentation

Use modern jsdoc style comments. Do not use old Doxygen-style comments. Only document the interface, not the implementation.

 1 // Correct.
 2 
 3 /** Sends a message to all connected clients. */
 4 void broadcast(const Message& message);
 5 
 6 /**
 7  * Establishes a connection to the server.
 8  * @param host The hostname to connect to.
 9  * @param port The port number.
10  * @return The established connection.
11  */
12 Connection connect(const std::string& host, int port);
13 
14 // Incorrect, old Doxygen style.
15 
16 /*! \brief Sends a message to all connected clients. */
17 void broadcast(const Message& message);

Character Set

Only basic ASCII characters are permitted in source code. Never use unicode characters.

Method Declaration Preference

When a helper function does not need access to class state, prefer placing it in the narrowest possible scope. In order of preference:

  1. Anonymous namespace (for file-local helpers in .cpp files)
  2. Private static member function
  3. Private member function
  4. Protected static member function
  5. Protected member function
  6. Free function
  7. Public static member function
  8. Public member function

Static Member Access

Static class members shall always be accessed through the class name, never through an instance.

1 // Correct.
2 auto value = Money::ONE;
3 auto result = Connection::make_default();
4 
5 // Incorrect, accesses static member through an instance.
6 auto connection = Connection();
7 auto result = connection.make_default();

Boolean Function Naming

Functions returning bool shall use a prefix that reads as a question: is_, has_, or can_.

1 bool is_empty(const Inventory& inventory);
2 bool has_order(OrderId id) const;
3 bool can_connect() const;

Function Bodies

Function bodies shall not contain blank lines. All statements within a function body are written without vertical separation.

 1 // Correct.
 2 void process(const Order& order) {
 3   auto fields = order.get_fields();
 4   auto quantity = fields.m_quantity;
 5   validate(fields);
 6   submit(order);
 7 }
 8 
 9 // Incorrect, blank lines within function body.
10 void process(const Order& order) {
11   auto fields = order.get_fields();
12   auto quantity = fields.m_quantity;
13 
14   validate(fields);
15 
16   submit(order);
17 }

Additional References

The bulk of this style guide focuses on syntax rather than good programming practices. The following list are reference materials (both online and published) for best practices on writing portable, efficient, and clean C++ code: