EthicsComputerCourses
Join training +91 90532 10052

Core Guidelines, clang-tidy & clang-format

C++ · Lesson 45 of 46 Advanced 8 min read Updated

In a company, your code is read many more times than it is written: by reviewers, by teammates fixing bugs, and by you six months later. Teams therefore agree on rules for how C++ should be written, and they let tools check those rules automatically so that code reviews can focus on design and logic instead of spaces and missing const.

This lesson introduces the C++ Core Guidelines, the most widely used set of modern C++ rules, and the two tools that enforce rules in practice: clang-tidy, which finds bug-prone and outdated code, and clang-format, which fixes layout.

The C++ Core Guidelines

The C++ Core Guidelines are a free, open document led by Bjarne Stroustrup (the creator of C++) and Herb Sutter, published at isocpp.github.io/CppCoreGuidelines. Each rule has an ID made of a section letter and a number, a reason, examples and, where possible, a note on how a tool can check it. The sections include P (philosophy), I (interfaces), F (functions), C (classes), R (resource management), ES (expressions and statements), E (error handling), Con (constants), CP (concurrency), Per (performance) and SF (source files).

You do not need to memorise hundreds of rules. These are the ones you will hear about most in code reviews:

RuleIn short
R.1Manage resources with RAII objects, not by hand
R.11Avoid calling new and delete yourself
R.3A raw pointer T* never owns what it points to
I.11Never pass ownership through a raw pointer or reference
C.21If you write or delete one of the copy, move or destructor functions, handle all of them (rule of five)
C.35A base class destructor is either public and virtual, or protected and non-virtual
ES.20Always initialise an object
ES.49If you must cast, use a named cast such as static_cast
Con.1Make objects const unless they need to change
F.16Pass cheap types by value and others by reference to const
CP.20Lock with RAII (std::lock_guard, std::scoped_lock), never plain lock()/unlock()
SF.7Never write using namespace at global scope in a header

The guidelines come with a small helper library, the Guidelines Support Library (GSL), which provides types such as gsl::not_null and gsl::owner. Many teams use the rules without the library.

The rules applied

Here is code typical of an older project. Count the problems before reading on.

C++
#include <vector>

class Order {
public:
    virtual ~Order() {}
    virtual double total() { return 0; }
};

class TrainTicket : public Order {
public:
    double fare;
    double total() { return fare; }
};

double sum(std::vector<Order*> orders) {
    double s;
    for (int i = 0; i < (int)orders.size(); i++) s += orders[i]->total();
    return s;
}

The problems: total() changes nothing but is not const (Con.1); the override has no override keyword, so a typo would silently create a new function; fare is public data that anyone can break; the vector is copied on every call (F.16); s is never initialised (ES.20), which is undefined behaviour; there is a C-style cast (ES.49); and nobody can tell who owns the Order* pointers (R.3, I.11). Here is the same code following the guidelines:

C++
#include <memory>
#include <print>
#include <span>
#include <vector>

class Order {
public:
    virtual ~Order() = default;
    virtual double total() const = 0;
};

class TrainTicket final : public Order {
public:
    explicit TrainTicket(double fare) : fare_{fare} {}
    double total() const override { return fare_; }
private:
    double fare_;
};

double sum(std::span<const std::unique_ptr<Order>> orders) {
    double s = 0.0;
    for (const auto& order : orders) {
        s += order->total();
    }
    return s;
}

int main() {
    std::vector<std::unique_ptr<Order>> orders;
    orders.push_back(std::make_unique<TrainTicket>(1299.0));
    orders.push_back(std::make_unique<TrainTicket>(455.5));
    std::println("Total: {:.2f}", sum(orders));
}
Output
Total: 1754.50

Ownership is now visible in the types (std::unique_ptr owns, std::span only looks), and the compiler checks the override.

clang-tidy: automatic code review

clang-tidy is a free tool from the LLVM project that reads your code like a compiler and reports problems from a list of several hundred checks. Many checks can also fix the code for you. Checks are grouped by prefix:

GroupFinds
bugprone-Code that is legal but very likely a bug, such as bugprone-use-after-move
cppcoreguidelines-Core Guidelines rules, such as cppcoreguidelines-init-variables
modernize-Old style that has a modern replacement, such as modernize-use-nullptr
performance-Needless copies, such as performance-unnecessary-value-param
readability-Confusing names, magic numbers, deeply nested code

clang-tidy needs to know how each file is compiled. CMake can write that information into compile_commands.json when you configure with -DCMAKE_EXPORT_COMPILE_COMMANDS=ON. Then run clang-tidy and point it at the build folder with -p:

Terminal
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
clang-tidy -p build src/orders.cpp
Output
src/orders.cpp:15:32: warning: the parameter 'orders' is copied for each invocation but only used as a const reference; consider making it a const reference [performance-unnecessary-value-param]
src/orders.cpp:16:12: warning: variable 's' is not initialized [cppcoreguidelines-init-variables]
...

The output is shortened, and the exact checks reported depend on your configuration. Each warning ends with the name of the check in square brackets, so you can look it up. Add --fix to let clang-tidy apply the fixes it knows, then review the changes with git diff.

Choose your checks in a .clang-tidy file at the top of the project; it is written in YAML:

YAML
Checks: >
  -*,
  bugprone-*,
  cppcoreguidelines-*,
  modernize-*,
  performance-*,
  -modernize-use-trailing-return-type,
  -cppcoreguidelines-avoid-magic-numbers
WarningsAsErrors: 'bugprone-*'
HeaderFilterRegex: 'include/.*'

-* first turns everything off, then each line turns a group on; a leading - turns one check off again. Some checks, such as the one that wants auto f() -> int everywhere, are a matter of taste, and most teams disable them. To run clang-tidy on every build, set CMAKE_CXX_CLANG_TIDY in CMake, for example set(CMAKE_CXX_CLANG_TIDY clang-tidy).

clang-format: no more arguments about spaces

clang-format rewrites the layout of your code (indentation, spaces, line breaks, brace positions, include order) to match a style. Nobody has to discuss formatting in reviews again, and diffs show only real changes. Put a .clang-format file at the top of the project:

YAML
BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 100

BasedOnStyle starts from a well-known style (others include LLVM, Chromium, Mozilla, WebKit and Microsoft), and the following lines adjust it. Format files in place with:

Terminal
clang-format -i src/*.cpp include/school/*.hpp

Most editors, including VS Code with the C/C++ extension, can run clang-format every time you save. Teams usually also check formatting automatically before code is merged.

Common mistakes

  • Turning on every check at once. Thousands of warnings get ignored. Start small and keep the count at zero.
  • Blindly applying --fix. Automatic fixes are usually right but not always; review and test them.
  • Mixing formatting and logic changes in one commit. Reviewers cannot see the real change.
  • Treating the guidelines as laws. Each rule has reasons and exceptions; when you break one, write a comment that explains why.
  • Running clang-tidy without compile_commands.json. It then guesses flags and include paths and reports false errors.

Key points

  • The C++ Core Guidelines are a free set of modern C++ rules with IDs such as R.11 and ES.20 that teams cite in reviews.
  • Key ideas: RAII, no owning raw pointers, initialise everything, const by default, named casts, override.
  • clang-tidy checks code against hundreds of rules; configure it with .clang-tidy and give it compile_commands.json.
  • clang-format fixes layout automatically from a .clang-format file and ends style debates.
  • Introduce tools step by step and keep warnings at zero.

Exercise

Review this function using the Core Guidelines. Write down each rule it breaks (an ID or a short description is enough), then rewrite it.

int* make_scores(int n) { int* p = new int[n]; for (int i = 0; i < n; i++) p[i] = 0; return p; }

Show solution
C++
#include <cstddef>
#include <print>
#include <vector>

std::vector<int> make_scores(int n) {
    return std::vector<int>(static_cast<std::size_t>(n), 0);
}

int main() {
    const auto scores = make_scores(4);
    std::println("{} scores, first is {}", scores.size(), scores.front());
}
Output
4 scores, first is 0

The original calls new directly (R.11), returns an owning raw pointer so the caller must remember delete[] (R.3, I.11), and a leak or the wrong delete form is easy. A std::vector owns its memory, knows its size and is returned efficiently. The result is const because it does not change (Con.1).

Quick Quiz

1. Which Core Guidelines idea does std::unique_ptr support most directly?

2. What does clang-tidy need to know how each file is compiled?

3. What does clang-format change?

4. In a .clang-tidy file, what does -* at the start of Checks do?

Learn C++ with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call