As a very large language used for a very wide range of purposes over many decades, C++ can be written in a lot of different ways. Today we’ll look at some of the norms for “modern” C++ to get a sense of how code is normally written.

Table of Contents

Guides

There are several existing, popular guides that aim to impose programming standards on C++ codebases for a variety of reasons. These reasons range from trivialities such as formatting to standardization of error-handling and outright bans on certain language features. Additionally, many teams and organizations will create their own in-house rules and possibly enforce them with tools like ClangFormat.

Here are some of the most popular public guides:

There are strong disagreements between these guides and even their scopes vary widely. This article is mostly about areas that have some semblance of agreement in the broader community of C++ developers. It’s not an attempt to create a new guide.

Use macros extremely rarely

Macros are evaluated at an early stage of compilation and essentially operate as scope-unaware text-replacers that aren’t fully compatible with either variables or functions. They’re notoriously difficult to debug and even understand. While they’re essential to implement assertions, there are very few other cases where they are preferable to regular C++ code:

// Avoid
#define PI 3.14
 
// Encourage
const float PI = 3.14;
// Avoid
#define SQUARE(x) x * x
 
// Encourage
float Square(float x)
{
    return x * x;
}
Add include guards to every header

As of this writing, modules are not yet in common usage. While still using the classic header file-based build system, every header file should have “include guards” to prevent redundant definitions by multiple #include directives:

// Avoid
struct Point2
{
    float X;
    float Y;
};
 
// Encourage
#ifndef POINT2_HPP
#define POINT2_HPP
struct Point2
{
    float X;
    float Y;
};
#endif
 
// Encourage (non-standard but widely-supported alternative)
#pragma once
struct Point2
{
    float X;
    float Y;
};
Include dependencies directly instead of relying on indirect includes

When one file uses code in another file, it should #include the file declaring that code directly rather than relying on another header to #include the desired code. This prevents compilation errors if the middle header removes its #include.

////////
// Avoid
////////
 
// a.h
struct A {};
 
// b.h
#include "a.h"
struct B : A {};
 
// c.h
#include "b.h"
A a; // Not in b.h
 
////////////
// Encourage
////////////
 
// a.h
struct A {};
 
// b.h
#include "a.h"
struct B : A {};
 
// c.h
#include "a.h"
A a;
Don’t call virtual functions in constructors

Virtual functions rely on a table that’s initialized by the constructors of the classes in an inheritance hierarchy. Calling virtual functions before these are set up can result in crashes or calling the wrong version of the function:

// Avoid
struct Parent
{
    Parent()
    {
        Foo();
    }
 
    virtual void Foo()
    {
        DebugLog("Parent");
    }
};
struct Child : Parent
{
    virtual void Foo() override
    {
        DebugLog("Child");
    }
};
Child c; // Prints "Parent" not "Child"!
 
// Encourage designs that do not require such calls
Don’t use variadic functions

Variadic functions aren’t type-safe, rely on error-prone macros, are difficult to optimize, and often result in error-prone APIs. They should be avoided in favor of techniques such as fold expressions or the use of container types:

// Avoid
void DebugLog(int count, ...)
{
    va_list args;
    va_start(args, count);
    for (int i = 0; i < count; ++i)
    {
        const char* log = va_arg(args, const char*);
        std::cout << log << ", ";
    }
    va_end(args);
}
DebugLog(4, "foo", "bar", "baz"); // Whoops! 4 reads beyond the last arg!
 
// Encourage
void DebugLog()
{
    std::cout << '\n';
}
template <typename T>
void DebugLog(const T& val)
{
    std::cout << val << '\n';
}
template <typename TFirst, typename TSecond, typename ...TRemain>
void DebugLog(const TFirst& first, const TSecond& second, TRemain... remain)
{
    std::cout << first << ", ";
    DebugLog(second, remain...);
}
DebugLog("foo", "bar", "baz"); // No need for a count. Can't get it wrong.
No naked new and delete

The new and delete operators to dynamically allocate memory should be a rare sight. Instead, “owning” types such as containers and smart pointers should call new in their constructors and delete in their destructors to ensure that memory is always cleaned up:

// Avoid
struct Game
{
    Stats* stats;
    Game()
        : stats{new Stats()}
    {
    }
    ~Game()
    {
        delete stats;
    }
};
 
// Encourage
struct Game
{
    std::unique_ptr<Stats> stats;
    Game()
        : stats{std::make_unique<Stats>()}
    {
    }
};
// Avoid
struct FloatBuffer
{
    float* floats;
    FloatBuffer(int32_t count)
        : floats{new float[count]}
    {
    }
    ~FloatBuffer()
    {
        delete [] floats;
    }
};
 
// Encourage
struct FloatBuffer
{
    std::vector<float> floats;
    FloatBuffer(int32_t count)
        : floats{count}
    {
    }
};
Prefer range-based loops

The most common loop is from the beginning to the end of a collection. To avoid mistakes and make this more terse, use a range-based) for loop instead of the three-part for loop, a while loop, or a do-while loop:

// Avoid
for (
    std::vector<float>::iterator it = floats.begin();
    it != floats.end();
    ++i)
{
    DebugLog(*it);
}
 
// Encourage
for (float f : floats)
{
    DebugLog(f);
}
Use scoped enums instead of unscoped enums

To avoid adding all the enumerators of an unscoped enum to the surrounding scope, use a scoped enumeration:

// Avoid
enum Colors { Red, Green, Blue };
uint32_t Red = 0x00ff00ff; // Error: redefinition because Red escaped the enum
 
// Encourage
enum class Colors { Red, Green, Blue };
uint32_t Red = 0x00ff00ff; // OK
Don’t breach namespaces in headers

When commonly using the members of a namespace, it can be convenient to pull them out with using namespace. When this is done in a header, the files that #include it have this decision forced on them. This can lead to namespace collisions and confusion, so it should be avoided.

// Avoid
using namespace std;
struct Name
{
    string First;
    string Last;
};
 
// Encourage
struct Name
{
    std::string First;
    std::string Last;
};
Make single-parameter constructors explicit

Constructors default to allowing implicit conversion, which can be surprising and expensive. Use the explicit keyword to disallow this behavior:

// Avoid
struct Buffer
{
    std::vector<float> floats;
    Buffer(int x)
        : floats{x}
    {
    }
};
void DoStuff(Buffer b)
{
}
DoStuff(1'000'000); // Allocates a Buffer of one million floats!
 
// Encourage
struct Buffer
{
    std::vector<float> floats;
    explicit Buffer(int x)
        : floats{x}
    {
    }
};
void DoStuff(Buffer b)
{
}
DoStuff(1'000'000); // Compiler error
Don’t use C casts

C-style casts may just change the type but also might perform value conversion. They’re hard to search for as they blend in with other parentheses. Instead, use a named C++ cast for better control and easier searching:

// Avoid
const float val = 5.5;
int x = (int)val; // Changes type, truncates, and removes constness!
DebugLog(x);
 
// Encourage
const float val = 5.5;
int x = const_cast<int>(val); // Compiler error
DebugLog(x);
Use specific integer sizes

All the way back in the second article of the series, we learned that the size guarantees for primitive types like long are very weak. They should be avoided in favor of guaranteed sizes in <cstdint>:

// Avoid
long GetFileSize(const char* path)
{
    // Does this support files larger than 4 GB?
    // Depends on whether long is 32-bit or 64-bit
}
 
// Encourage
int64_t GetFileSize(const char* path)
{
    // Definitely supports large files
}
Use nullptr

NULL is an implementation-defined variable-like macro that even requires a #include to use. It’s a null pointer constant, but of unknown type and erroneously usable in arithmetic. In contrast nullptr is not a macro or an integer, requires no header, and even has its own type which can be used in overload resolution. It should be used to represent null pointers:

// Avoid
int* p = NULL;
 
// Encourage
int* p = nullptr;
Follow the Rule of Zero

Most classes shouldn’t need any copy constructors, move constructors, destructors, copy assignment operators, or move assignment operators. Instead, their data members should take care of these functions. This is called the “rule of zero” because no special functions need to be added. It’s the simplest approach and the hardest to implement incorrectly:

// Avoid
struct Player
{
    std::string Name;
    int32_t Score;
 
    Player(std::string name, int32_t score)
        : Name{ name }
        , Score{ score }
    {
    }
 
    Player(const Player& other)
        : Name{ other.Name }
        , Score{ other.Score }
    {
    }
 
    Player(Player&& other) noexcept
        : Name{ std::move(other.Name) }
        , Score{ std::move(other.Score) }
    {
    }
 
    virtual ~Player()
    {
    }
 
    Player& operator=(const Player& other)
    {
        Name = other.Name;
        Score = other.Score;
        return *this;
    }
 
    Player& operator==(Player&& other)
    {
        Name = std::move(other.Name);
        Score = std::move(other.Score);
        return *this;
    }
};
Player p{ "Jackson", 1000 };
 
// Encourage
struct Player
{
    std::string Name;
    int32_t Score;
};
Player p{ "Jackson", 1000 };
Follow the Rule of Five

In cases where the Rule of Zero can’t be followed and a special function needs to be added, add all five of them to handle all the ways that objects can be copied, moved, and destroyed:

// Avoid
struct File
{
    std::string Path;
    const char* Mode;
    FILE* Handle;
 
    File(std::string path, const char* mode)
        : Path{ path }
        , Mode{ mode }
        , Handle(fopen(path.c_str(), mode))
    {
    }
 
    File(const File& other)
        : Path{ other.Path }
        , Mode{ other.Mode }
        , Handle(fopen(other.Path.c_str(), other.Mode))
    {
    }
 
    virtual ~File()
    {
        if (Handle)
        {
            fclose(Handle);
        }
    }
 
    File& operator=(const File& other)
    {
        Path = other.Path;
        Mode = other.Mode;
        Handle = fopen(other.Path.c_str(), other.Mode);
        return *this;
    }
 
    // No move constructor or move assignment operator
    // Expensive copies will be required: more file open and close operations!
};
 
// Encourage
struct File
{
    std::string Path;
    const char* Mode;
    FILE* Handle;
 
    File(std::string path, const char* mode)
        : Path{ path }
        , Mode{ mode }
        , Handle(fopen(path.c_str(), mode))
    {
    }
 
    File(const File& other)
        : Path{ other.Path }
        , Mode{ other.Mode }
        , Handle(fopen(other.Path.c_str(), other.Mode))
    {
    }
 
    File(File&& other) noexcept
        : Path{ std::move(other.Path) }
        , Mode{ other.Mode }
        , Handle(other.Handle)
    {
        other.Handle = nullptr;
    }
 
    virtual ~File()
    {
        if (Handle)
        {
            fclose(Handle);
        }
    }
 
    File& operator=(const File& other)
    {
        Path = other.Path;
        Mode = other.Mode;
        Handle = fopen(other.Path.c_str(), other.Mode);
        return *this;
    }
 
    File& operator==(File&& other)
    {
        Path = std::move(other.Path);
        Mode = other.Mode;
        Handle = other.Handle;
        other.Handle = nullptr;
        return *this;
    }
};
Avoid raw loops

Hand-implemented algorithms are error-prone and difficult to read. Many common algorithms, and even parallelized versions, are implemented in the algorithms library and ranges library for us and can be used with a broad number of types. Readers of such code encounter just a named algorithm which they’re likely already familiar with rather than needing to interpret that from a possibly-complex loop.

// Avoid
struct Player
{
    const char* Name;
    int NumPoints;
};
void Avoid(const std::vector<Player>& players)
{
    using It = std::reverse_iterator<std::vector<Player>::const_iterator>;
    for (It it = players.rbegin(); it != players.rend(); ++it)
    {
        const Player& player = *it;
        if (player.NumPoints > 25)
        {
            Player copy = player;
            copy.NumPoints--;
            DebugLog(copy.Name, copy.NumPoints);
        }
    }
}
 
// Encourage
struct Player
{
    const char* Name;
    int NumPoints;
};
void Encourage(const std::vector<Player>& players)
{
    using namespace std::ranges::views;
 
    auto result =
        players
        | filter([](Player p) { return p.NumPoints > 25; })
        | transform([](Player p) { p.NumPoints--; return p; })
        | reverse;
    for (const Player& p : result)
    {
        DebugLog(p.Name, p.NumPoints);
    }
}
Add restrictions

When using objects in a read-only way, make them const. When code can usefully run at compile time, make it constexpr. When a function can’t throw any exceptions, make it noexcept. When fields don’t need to be used outside of a class, make them protected or private. When derivation or overriding are undesirable, make classes and member functions final. All of these restrictions will add compiler-enforced rules that prevent misuse such as field access or enable new uses such as compile-time code execution.

// Avoid: requires writable strings, can't be used at compile time, might throw
int32_t GetTotalCharacters(std::string& first, std::string& last)
{
    return first.size() + last.size();
}
 
// Encourage
constexpr int32_t GetTotalCharacters(
    const std::string& first, const std::string& last) noexcept
{
    return first.size() + last.size();
}
Use braced initialization

Braced initialization (x{} or x = {}) is always clearly initialization. Other forms of initialization such as with parentheses (x()) or nothing (x) are much more ambiguous. They can be mistaken for declarations, function calls, and function-style casts. Prefer braced initialization to ensure that initialization occurs:

// Avoid
template <typename T>
T GetDefault()
{
    T t; // Default constructor for classes but nothing for primitives
    return t;
}
struct Point2
{
    float X{ 0 };
    float Y{ 0 };
};
std::ostream& operator<<(std::ostream& s, const Point2& p)
{
    s << p.X << ", " << p.Y;
    return s;
}
DebugLog(GetDefault<Point2>()); // 0, 0
DebugLog(GetDefault<int>()); // undefined behavior!
 
// Encourage
template <typename T>
T GetDefault()
{
    T t{}; // Default constructor for classes, primitives are value-initialized
    return t;
}
DebugLog(GetDefault<Point2>()); // 0, 0
DebugLog(GetDefault<int>()); // 0
Standardize error-handling

There are two main choices for error-handling in C++: exceptions and error codes. C’s errno isn’t considered a valid choice due to its reliance on global state which is not part of the call signature and not thread-safe. Codebases should choose one approach or the other to handle errors consistently and safely. For example, introducing exceptions into a codebase that uses error codes is likely to cause uncaught exceptions that crash the program.

If exceptions are chosen, they should be thrown by value and caught by const reference:

// Avoid
try
{
    throw new std::runtime_error{ "Boom!" };
}
catch (std::runtime_error* err)
{
    DebugLog(err->what()); // Boom!
    // ... memory leak here ...
}
 
// Encourage
try
{
    throw std::runtime_error{ "Boom!" };
}
catch (const std::runtime_error& err)
{
    DebugLog(err.what()); // Boom!
}

If error codes are chosen, a wrapper type such as std::optional is encouraged over the use of null pointers to clearly indicate to callers that the operation may not succeed. The use of [[nodiscard]] is also often warranted to ensure errors are handled.

////////
// Avoid
////////
 
// Caller doesn't know what happens upon error
// Caller can ignore error return values
FILE* OpenFile(const char* path, const char* mode)
{
    return fopen(path, mode);
}
 
FILE* handle = OpenFile("/path/to/file", "rw");
fprintf(handle, "Hello!"); // Crash if null is returned
fclose(handle);
 
////////////
// Encourage
////////////
 
// Caller clearly knows this can fail due to the std::optional return value
// Caller can't ignore it due to the [[nodiscard]] attribute
[[nodiscard]] std::optional<FILE*> OpenFile(const char* path, const char* mode)
{
    FILE* handle = fopen(path, mode);
    if (!handle)
    {
        return {};
    }
    return handle;
}
 
// Handling the return value is required by [[nodiscard]]
std::optional<FILE*> result = OpenFile("/path/to/file", "rw");
if (!result.has_value())
{
    DebugLog("Failed to open file");
    return;
}
 
// Can't directly use the result. Forced to deal with it being optional.
// Fewer chances to dereference null and crash
FILE* handle = result.value();
fprintf(handle, "Hello!");
fclose(handle);
Mark overridden member functions with override

A virtual member function that overrides a base class’ member function doesn’t have to be marked that way, but it’s helpful to indicate this. It provides a keyword that readers of the code can look for to know how the function fits into the class design. It also provides the compiler with a way to enforce that the function really overrides a base class version. If the function signatures subtlely don’t match or the base class no longer has such a function, the compiler will catch the mistake instead of creating a new function.

// Avoid
struct Animal
{
    virtual void Speak(const char* message, bool loud=false)
    {
        // By default, animals can't speak
    }
};
struct Dog : Animal
{
    // Missing "loud" parameter creates a new function
    virtual void Speak(const char* message)
    {
        DebugLog("woof: ", message);
    }
};
std::unique_ptr<Animal> a = std::make_unique<Dog>();
a->Speak("go for a walk?"); // Prints nothing because Dog doesn't override
 
// Encourage
struct Animal
{
    virtual void Speak(const char* message, bool loud=false)
    {
        // By default, animals can't speak
    }
};
struct Dog : Animal
{
    // Missing "loud" parameter is a compiler error
    virtual void Speak(const char* message) override
    {
        DebugLog("woof: ", message);
    }
};
std::unique_ptr<Animal> a = std::make_unique<Dog>();
a->Speak("go for a walk?"); // Never executed due to compiler error
Use using, not typedef

C’s typedef alias is still supported, but using is a strictly better version of it. The alias and the target are put into the familiar assignment form where the left hand side is assigned to from the right hand side. It also supports being templated, so it fits in better with generic programming.

// Avoid
typedef float f32;
f32 pi = 3.14f;
 
// Encourage
using f32 = float;
f32 pi = 3.14f;
// Avoid
#define VEC(T) std::vector<T>
VEC(float) floats;
 
// Encourage
template <typename T> using Vec = std::vector<T>;
Vec<float> floats;
Minimize function definitions in header files

Header files are typically compiled many times as many translation units directly or indirectly #include them. Any changes to the header file will require recompiling all the translation units that #include it. The linker will eventually de-duplicate these, but the compilation is slow and so build and iteration times suffer. To reduce the time it takes to compile header files, reduce the number of function definitions in them. Instead, declare functions in them and define them in translation units whenever possible.

////////
// Avoid
////////
 
// math.h
#pragma once
bool IsNearlyZero(float x)
{
    return std::abs(x) < 0.0001f;
}
 
////////////
// Encourage
////////////
 
// math.h
#pragma once
bool IsNearlyZero(float x);
 
// math.cpp
#include "math.h"
bool IsNearlyZero(float x)
{
    return std::abs(x) < 0.0001f;
}
Use internal linkage for file-specific definitions

By default, entities like variables, functions, and classes have external linkage at file scope. This slows down compilation and the linker because they need to consider the possibility that some other translation unit might want to reference those entities. To speed it up, use static or an unnamed namespace to give those entities internal linkage and remove their candidacy for reference by other translation units.

// Avoid
float PI = 3.14f;
 
// Encourage
static float PI = 3.14f;
 
// Encourage
namespace
{
    float PI = 3.14f;
}
Use operator overloading and user-defined literals very sparingly

Overloaded operators don’t really get a name and user-defined literals usually only have a terse one. As such, it’s often hard for readers to understand what they’re doing. Even worse, overloaded operators may appear to have one meaning while the implementation of the overloaded operator does something else. These should generally be avoided except in cases where the meaning is already well-understood. For example, the + operator on two std::string objects is clearly concatenation of the left hand operand followed by the right hand operand but the + operator on two Player objects is quite a puzzle.

// Avoid
struct Player
{
    int32_t Points;
 
    Player operator+(const Player& other)
    {
        return { Points + other.Points };
    }
};
Player a{ 100 };
Player b{ 200 };
Player c = a + b; // No conventional meaning for what + does
DebugLog(c.Points); // 300
 
// Encourage
struct Vector2
{
    float X;
    float Y;
 
    Vector2 operator+(const Vector2& other)
    {
        return { X + other.X, Y + other.Y };
    }
};
Vector2 a{ 100, 200 };
Vector2 b{ 300, 400 };
Vector2 c = a + b; // Well-understood mathematical operator
DebugLog(c.X, c.Y); // 400, 600
Prefer pre-increment to post-increment

Whether we use the pre-increment operator (++x) or the post-increment operator (x++) on a primitive type like int makes no difference. With classes that have overloaded this operator, especially in the case of iterators, the pre-increment operator can be implemented more efficiently by removing the need to temporarily have two copies. It’s generally preferable to use the pre-increment operator for this reason:

// Avoid: potentially slower than pre-increment
for (auto it = floats.begin(); it != floats.end(); it++)
{
    DebugLog(*it);
}
 
// Encourage: always the fastest way to increment
for (auto it = floats.begin(); it != floats.end(); ++it)
{
    DebugLog(*it);
}
Avoid template metaprogramming

The vast majority of code written should steer far clear from advanced features such as template metaprogramming. This umbrella term refers to using templates as the Turing-complete language they are to generate very complex code at compile time. Techniques such as SFINAE, not even covered in this series, should generally be the province of a few expert-level library creators such as those implementing the C++ Standard Library, testing frameworks, serialization libraries, and so forth. Almost all “normal” code should stick to “normal” features.

// Avoid: SFINAE like this and other TMP tricks
template<
    typename T,
    std::enable_if_t<std::is_integral<T>::value, bool> = true>
void PrintKindOfPrimitive(T)
{
    DebugLog("it's an integer");
}
template<
    typename T,
    std::enable_if_t<std::is_floating_point<T>::value, bool> = true>
void PrintKindOfPrimitive(T)
{
    DebugLog("it's a float");
}
PrintKindOfPrimitive(123); // it's an integer
PrintKindOfPrimitive(3.14); // it's a float
 
// Encourage using libraries that implement this or avoiding the need at all
Use auto for at least long type names

Some codebases prefer the AAA style: “almost always auto.” This means the type of most variables is auto and only in a few cases is an explicit type named. Advantages include terseness and the potential avoidance of type conversion, especially when types are changed in existing code. Other codebases prefer to explicitly name all types. Advantages include readability without the need for IDE tooltips that reveal deduced types, such as when looking at code in a web browser.

Regardless of the decision, and both are popular, both camps tend to agree that very long types are hard to read and often made more clear by the use of auto:

// Avoid: type name is so long that an alias is required to fit it on one line
using Map = std::unordered_map<std::basic_string<char8_t>, Game::Player*>;
using It = Map::const_iterator;
for (It it = players.begin(); it != players.end(); ++it)
{
    DebugLog(it->first, "has", it->second->Points, "points");
}
 
// Encourage: use auto for at least long types like these
for (auto it = players.begin(); it != players.end(); ++it)
{
    DebugLog(it->first, "has", it->second->Points, "points");
}
Use compile-time polymorphism more often

Both compile-time polymorphism with templates and run-time polymorphism with virtual functions have valid use cases. However, most languages have limited support for compile-time polymorphism. As such, we can often overlook possibilities for performance improvements by shifting run-time work to compile-time. A lot of code that’s knowable at compile-time must be determined at run-time in languages like C# while the opportunity is there in C++ to make the determination at compile-time. Idiomatic C++ tends to prefer these compile-time solutions to improve run-time performance:

// Avoid: run-time polymorphism when compile-time is suitable
struct Weapon
{
    virtual void DoDamage(Player& player) = 0;
};
struct FoamDart : Weapon
{
    virtual void DoDamage(Player& player) override
    {
        player.Health--;
    }
};
struct Bazooka : Weapon
{
    virtual void DoDamage(Player& player) override
    {
        player.Health = 0;
    }
};
FoamDart w;
w.DoDamage(p); // Run-time decision
 
// Encourage: compile-time polymorphism when suitable
struct FoamDart
{
    void DoDamage(Player& player)
    {
        player.Health--;
    }
};
struct Bazooka
{
    void DoDamage(Player& player)
    {
        player.Health = 0;
    }
};
template <typename TWeapon>
void DoDamage(const TWeapon& weapon, Player& player)
{
    weapon.DoDamage(player);
}
FoamDart w;
DoDamage(w, player); // Compile-time decision
Conclusion

None of the above is gospel. There are exceptional cases for all of these guidelines. Nearly all guides will make some different choices than the above. Despite the conflicting guidance, all of the above are common advice in many guides, teams, codebases, or organizations. Still, a lot is surely missing. It’s not feasible to list every best practice or idiom for C++ any more than it’s feasible for a much smaller, newer language like C#.

In the end, we’re not working on some abstract code. We’ll work on particular codebases and it’s important to be aware of the norms of those particular environments. Each will have their own written or unwritten rules, not to mention very subjective thoughts on style such as the placement of curly braces and whether indentation should be done with tabs or spaces. These certainly aren’t C++-specific issues, but in the case of C++ it’s wide use, large size, and long history somewhat increase the challenge.