Skip to content

Coding guidelines

Rapteon edited this page Feb 27, 2025 · 11 revisions

C++ Coding Guidelines

All of these tips are aggregated from learncpp.com.

  1. Do not use using directives such as using namespace std at the beginning of each C++ file. This is considered bad practice.

  2. Function macros are generally considered unsafe to use. They can be replaced by normal functions.

    // Do not create this type of macro.
    #define SQUARE(x) ((x) * (x))
    int main() {
        // Do not use something like this
        int result = SQUARE(5);  // Expands to (5 * 5)
    }
  3. Object-like macros should be avoided as much as possible. There are better ways to do the same thing, like using compile-time constants. See this.

    // Do not use statements like the following. There are better options to get the same functionality.
    #define MAX_IO_VAL 3600 
    
    // However, these types of directives can be used (no value)
    #define RT_MODE
    
    int main() {
        bool running_in_rt {false};
    
        // The above define for RT_MODE can be used with #ifdef, #ifndef and #endif
        #ifdef RT_MODE
        running_in_rt = true;
        #endif
    }

    See more examples of usage here.

  4. Do not put function and variable definitions in your header files, unless you know that adding them will not violate the One Definition Rule (ODR).

    // File: light.h
    
    // Do not define functions in the header file.
    bool isLightOn() {
        return MAX_IO_VAL < 3000;
    }

    However, store only things like Types and declarations in header files.

    // File: light.h
    
    bool isLightOn();
  5. Source files (.cpp files) should "#include" their associated header files.

    // File: light.h
    
    bool isLightOn();

    And its source file:

    // File: light.cpp
    #include "light.h"
    
    bool isLightOn() {
        return true;
    }
  6. Do not "#include" .cpp files in your code.

    #include "calc.cpp"
    #include <iostream>
    
    int main() {
        std::cout << add(4, 2) << '\n';
        std::cout << subtract(4, 2) << '\n';
        std::cout << multiply(4, 2) << '\n';
        std::cout << divide(4, 2) << '\n';
        return 0;
    }
  7. Use the same name for the source file and its associated header file, for example, if the source file is add.cpp, then the header file should be add.h.

  8. Do not use relative paths for including header files in your code, for example, don't do something like
    #include "../headers/calc.h"
    Instead of doing this, configure your compiler to look for header files in a specific directory.
    This can be done by specifying an include path.
    Like this: g++ -o main -I./source/includes main.cpp. Note: there's no space after the -I.

  9. Please use header guards (enclosing header file contents between #ifndef+#define and #endif directives) to ensure the definitions from the header file are not duplicated. For example:

    #ifndef CALC_H
    #define CALC_H
    
    int add(int x, int y);
    int subtract(int x, int y);
    int multiply(int x, int y);
    int divide(int x, int y);
    #endif

    Alternatively, you can use the #pragma once directive at the top of the header file. Like so:

    #pragma once
    
    int add(int x, int y);
    int subtract(int x, int y);
    int multiply(int x, int y);
    int divide(int x, int y);
  10. Use const variables for values which are not allowed to change, like the sensor address or other physical quantities.

    ...
    
    const int MAX_TEMP { 35 }; // Required temperature in degree celsius.
    
    int main() {
        if (environment.getTemp() > MAX_TEMP) {
            std::cout << "Cannot start sensor.\n";
            return EXIT_FAILURE;
        } 
    }
  11. Use std::string_view instead of std::string type when you need a read-only string. This is because std::string
    types get copied when passed around or used for assignment to newly created variables. Requires C++17

    #include<iostream>
    #include<string_view> // Required to use std::string_view
    
    int main() {
        std::string_view someObject {"Car"};
        std::cout << "A  " << someObject << " is something.\n";
        return EXIT_SUCCESS;
    }
  12. Variables which are used only within a loop (for, while or do-while loop) must be defined within the loop body.

    #include<iostream>
    
    int main() {
        int i;  // Do not do this:
        for (int i = 0; i < 10; ++i) {
            std::cout << i << '\n';
        }
        /* The problem with initialising `i` outside the loop
           is that it remains accessible even after the loop
           has terminated, thus wasting memory, and introducing
           a possibility of some kind of accidental bug if `i`
           is used for something.
        */
    }
  13. Define global variables in a namespace or prefix them with g_ to clearly separate global variables from local variables.
    Do not define non-const global variables. Non-const global variables (those without the const or constexpr keywords
    can be manipulated by other any code within the file and this can make debugging very, very hard.
    An example showing good use of globals is shown below.

       #include <cstdlib>
       #include <iostream>
       #include <string_view>
    
       namespace Test {
           constexpr std::string_view some_random_string{"Some random string."};
           constexpr int some_random_number{42};
       } // namespace Test
    
       const int g_another_number = 56;
    
       int main() {
           std::cout << Test::some_random_string << '\n';
           std::cout << Test::some_random_number << '\n';
           std::cout << g_another_number << '\n';
           return EXIT_SUCCESS;
       }

Tips

  1. You can either comment a piece of code if you are unsure of it and don't want to remove it, or if that piece of code is required, but not right now, then you can use the #if 0 and #endif to enclose the statements causing the issue:

    int main() {
        waitForInput();
    
        commitOperation();
    
        /*
        The following code is disabled for hardware v2. v2 hardware doesn't require rollback upon failure as it is handled in hardware.
        */
        #if 0
        rollbackIfFailed();
        #endif
    }
  2. The headers for each grouping should be sorted alphabetically (unless the documentation for a 3rd party library instructs you to do otherwise). Order your #include statements like so:

    1. The paired header file for this code file (e.g. add.cpp should #include "add.h")
    2. Other headers from the same project (e.g. #include "mymath.h")
    3. 3rd party library headers (e.g. #include <boost/tuple/tuple.hpp>)
    4. Standard library headers (e.g. #include <iostream>)
  3. Add TODO comments in your code if you have not implemented something. For example // TODO: Implement efficient version of this function. Most IDEs support searching for TODO comments and will notify other developers of unimplemented functionality.

  4. Focus on writing maintainable code, and optimize only when and where the benefit will be substantive.

  5. If space is not an issue, use double instead of float for floating point numbers.

  6. If you want to store binary numbers, you can use the 0b prefix before
    an integral type (int, uint, etc.). However, when using std::cout to print them, you
    won't see the numbers binary format. Also see next tip for std::bitset which can be used to print binary numbers. Requires C++14

    int main() {
        int registerAddr {0b1001};
        std::cout << registerAddr << '\n'; // Will print 9, NOT 1001
        return EXIT_SUCCESS;
    }
  7. To print binary numbers, you can use the std::bitset<size> type. See the example file below:
    Requires C++ 14.

    #include<iostream>
    #include<bitset>
    
    int main() {
        std::bitset<4> registerAddr {0b1001};
        std::cout << registerAddr << '\n'; // Will print 1001, NOT 9
        return EXIT_SUCCESS;
    }
  8. C++ includes support for default function parameters. This means it is possible to define the default value of a parameter
    in a function, and this makes it possible for the function to be called without passing the value for the default parameter. Default parameters can be used when the function is usually called with the same arguments most of the time, or when a value is commonly passed by callers for one of the parameters.

    #include <cstdlib>
    #include <iostream>
    
    using namespace std::literals;
    
    constexpr std::string_view default_output{"Something"};
    
    void printSomething(std::string_view s_view = default_output) {
        std::cout << s_view << '\n';
    }
    int main() {
        printSomething();
        printSomething("National Vote"sv);
        return EXIT_FAILURE;
    }
  9. If you notice that you are writing the same logic within multiple overloaded functions, but which only differ in their
    parameter types, you can consider using template functions. See the example given below.

    #include <cstdlib>
    #include <iostream>
    
    template <typename T> // This declares the "T" is a template type.
    T sum(T x, T y) { return x + y; }
    
    int main() {
        // Calling sum<int> causes the compiler to create a version of sum() which takes in `int` arguments.
        std::cout << sum<int>(2, 3) << '\n';
        std::cout << sum<double>(2.1, 3.5) << '\n';
        return EXIT_FAILURE;
    }

Clone this wiki locally