C++ Core Guidelines: Philosophy

General guiding principles for implementing better code

Zach Wolpe
8 min readJan 29, 2024

The C++ core guidelines offer a fantastic way to think about building great software. The guidelines provide:

General principles for building less error-prone, simpler & more maintainable software.

Although directed at C++ it is a mistake to understate their broader utility.

The guidelines prioritise delivering safe, fast, statically type-safe code, that has no resource leaks and catches many programming logic errors.

“You can afford to do things right.”

Best of all, the guidelines are implementable — designed for gradual introduction into a code base. Consider these rules ideals for new code, opportunities to exploit when working on older code, and try to approximate these ideals as closely as feasible.

Why bother with rules?

a program that uses a rule is less error-prone and more maintainable than one that does not. Rules also usually lead to faster/easier initial development.

Meta Rules

The meta-rules (principles that produce the rules) are as follows:

  1. Reduce the likelihood of accidents, and common sources of C++ errors.
  2. Rules are not minimal or orthogonal.
  3. Avoid a long list of special cases by keeping rules general.
  4. The rules are not value-neutral. They are meant to make code simpler and more correct/safer than most existing C++ code, without loss of performance.
  5. Rules are meant to follow the zero-overhead principle (“what you don’t use, you don’t pay for” or “when you use an abstraction mechanism appropriately, you get at least as good performance as if you had hand-coded using lower-level language constructs”).
  6. They are meant to inhibit perfectly valid C++ code that correlates with errors, spurious complexity, and poor performance.
  7. The rules are not perfect. The aim is to do the most good for most programmers.

C++ Core Guidelines Philosophy

Philosophical rules are generally not mechanically checkable. Instead of offering a direct implementable solution, they form the rationale for more concrete (specific and checkable) rules.

The guidelines are available here, below is my summary.

P.1: Express ideas directly in code

Programmers neglect design documents and comments. What is expressed explicitly in code can be checked by compilers and static analysis tools.

class Date {
public:
Month month() const; // do - explicit about return type & cannot modify Date's state.
int month(); // don't
// ...
};

Express what is to be done, rather than just how it is done.

// Bad example -------------------------------------->>

change_speed(double s); // bad: what does s signify?
// ...
change_speed(2.3); // new speed or delta on old speed?)


// Good example ------------------------------------->>

change_speed(Speed s); // better: the meaning of s is specified
// ...
change_speed(2.3); // error: no unit
change_speed(23_m / 10s); // meters per second

Use standard packages instead of writing personalised code that is purely a replication. This is cleaner, more succinct and simpler.

P.2: Write in ISO Standard C++

Follow best practices.

P.3: Express intent

Unless the intent of some code is stated, it is impossible to tell whether the code does what it is supposed to do.

Say what should be done, rather than just how it should be done.

// Bad Example --------------------------------------------->>

// - intent is not expressed
// - i outlives the scope of the loop
// - detail of an index is exposed which may be misused.
gsl::index i = 0;
while (i < v.size()) {
// ... do something with v[i] ...
}

// Better Example ----------------------------------------->>

// - no reference to the iteration mechanism
// - const is referred to, modification is impossible.
for (const auto& x : v) { /* do something with the value of x */ }

// If modification is desired:
for (auto& x : v) { /* modify x */ }

// Standard Lib Example ----------------------------------->>

// using the standard library
for_each(v, [](int x) { /* do something with the value of x */ });

// if order is irrelevant
for_each(par, v, [](int x) { /* do something with the value of x */ });

For a more concrete example, consider an algorithm that requires coordinates of a 2D point

draw_line(int, int, int, int);  // obscure: (x1,y1,x2,y2)? (x,y,h,w)? ...?
// need to look up documentation to know

draw_line(Point, Point); // clearer

Look for common patterns for which there are better alternatives.

P.4: Ideally, a program should be statically type-safe

Ideally, a program would be completely statically (compile-time) type-safe.

Problem areas (causing crashes and security violations):

  • unions
  • casts
  • array decay
  • range errors
  • narrowing conversions

C++ specific alternatives:

  • unions — use variant (in C++17)
  • casts — minimize their use; templates can help
  • array decay — use span (from the GSL)
  • range errors — use span
  • narrowing conversions — minimize their use and use narrow or narrow_cast (from the GSL) where they are necessary

P.5: Prefer compile-time checking to run-time checking

You don’t need to write error handlers for errors caught at compile time. Prioritising compile-time checking over runtime checking provides code clarity and performance.

Don’t postpone to run time what can be done well at compile time.

// Bad code ----------------------------------------------------->>

// Overflow is underfined.
// Int is an alias used for integers
int bits = 0; // don't: avoidable code
for (Int i = 1; i; i <<= 1)
++bits;
if (bits < 32)
cerr << "Int too small\n";

// Good code ----------------------------------------------------->>

// static analysis
static_assert(sizeof(Int) >= 4); // do: compile-time check

Another example:

// Bad code ----------------------------------------------------->>
void read(int* p, int n); // read max n integers into *p

int a[100];
read(a, 1000); // bad, off the end


// Good code --------------------------------------------------->>
void read(span<int> r); // read into the range of integers r

int a[100];
read(a); // better: let the compiler figure out the number of elements
  • Avoid pointer arguments.
  • Avoid run-time checks for range violations.

P.6: What cannot be checked at compile time should be checkable at run time

It is impossible to catch all errors at compile time and often not affordable to catch all remaining errors at run time.

Write programs that in principle can be checked, given sufficient resources (analysis programs, run-time checks, machine resources, time).

// Bad code -------------------------------------------------->>

// Here static analysis is infeasible and dynamic checking can be very difficult,
// because using a pointer argument omits the number of elements.
// separately compiled, possibly dynamically loaded
extern void f(int* p);
void g(int n)
{
// bad: the number of elements is not passed to f()
f(new int[n]);
}


/*
Bad but better code ------------------------------------------>>
Pass the number of arguments as an argument.
Passing the number of elements as an argument is better (and far more common)
than just passing the pointer and relying on some (unstated) convention
for knowing or discovering the number of elements
*/
extern void f2(int* p, int n);
void g2(int n)
{
f2(new int[n], m); // bad: a wrong number of elements can be passed to f()
}
// This approach is vulnerable to errors due to a minor typo.



// Better code ---------------------------------------------->>

extern void f4(vector<int>&); // separately compiled, possibly dynamically loaded
extern void f4(span<int>); // separately compiled, possibly dynamically loaded
// NB: this assumes the calling code is ABI-compatible, using a
// compatible C++ compiler and the same stdlib implementation
void g3(int n)
{
vector<int> v(n);
f4(v); // pass a reference, retain ownership
f4(span<int>{v}); // pass a view, retain ownership
}

Flag (pointer, count) — style interfaces.

P.7: Catch run-time errors early

Avoid “mysterious” crashes. Avoid unexpected incorrect downstream results.

/*
---- BAD CODE ------------------------------------------------>>

- The (pointer, count)-style interface cannot mandate valid input.
- "out-of-range" errors are likely.
- We only catch an error after running for x iterations (manipulating the data).
*/

void increment1(int* p, int n) // bad: error-prone
{
for (int i = 0; i < n; ++i) ++p[i];
}

void use1(int m)
{
const int n = 10;
int a[n] = {};
// ...
increment1(a, m); // maybe typo, maybe m <= n is supposed
// but assume that m == 20
// ...
}


/*
---- BETTER CODE ------------------------------------------------>>

Check earily (before running until out of bounds).

*/
void increment2(span<int> p)
{
for (int& x : p) ++x;
}

void use2(int m)
{
const int n = 10;
int a[n] = {};
// ...
increment2({a, m}); // maybe typo, maybe m <= n is supposed
// ...


/*
---- BEST CODE -------------------------------------------------->>

Simplify the code & do not repeatedly enter the same argument (number of elements).
*/

void use3(int m)
{
const int n = 10;
int a[n] = {};
// ...
increment2(a); // the number of elements of a need not be repeated
// ...
}

P.8: Don’t leak any resources

Any resource leak is dangerous. Particularly in long-running programs.

// Bad code ---------------------------------------------->>
void f(char* name)
{
FILE* input = fopen(name, "r");
// ...
if (something) return; // bad: if something == true, a file handle is leaked
// ...
fclose(input);
}


// Good code --------------------------------------------->>

void f(char* name)
{
ifstream input {name};
// ...
if (something) return; // OK: no leak
// ...
}

A leak is not restricted to RAM or CPU usage. A leak is “anything that isn’t (and can no longer be) cleaned up”.

Watch out for dangling pointers.

Note: relying on known guaranteed cleanup such as file closing and memory deallocation upon process shutdown can simplify code — there is no reason to not make use of this.

P.9: Don’t waste time or space

This is C++.

Time and space that you spend well to achieve a goal (e.g., speed of development, resource safety, or simplification of testing) is not wasted.

“Another benefit of striving for efficiency is that the process forces you to understand the problem in more depth.” — Alex Stepanov

Waste and complexity compound. Individual examples of waste are rarely significant and easily rectified. Waste spread sporadically over a large codebase can become very significant and be difficult to improve efficiently (if not only unrealistic to go back and refactor).

Tip: flag an unused return value from a user-defined non-defaulted postfix operator ++ or -- function, prefer the prefix form.

P.10: Prefer immutable data to mutable data

It’s easier to reason about constants than about variables. Immutable objects cannot change unexpectedly or fall victim to race conditions. Immutable objects often enable better optimization.

P.11: Encapsulate messy constructs, rather than spreading through the code

Messy code is more likely to contain bugs. A good interface is easier and safer to use.

Low-level code is particularly vulnerable to minor errors.

P.12: Use supporting tools as appropriate

We have better things to do than repeatedly do routine tasks.

Warning: be wary of over-elaborate or over-specialized tool chains, that make your otherwise portable code non-portable.

P.13: Use support libraries as appropriate

If a suitable library exists, use it.

Well-designed, well-documented, and well-supported library save time and effort.

It is unlikely that you will match the quality and documentation of major libraries — as most of your time will be allocated to producing an implementation. A widely used library is also more likely to be maintained and ported to new systems.

Enforceability: You need a reason not to use the standard library (or whatever foundational libraries your application uses) rather than a reason to use it.

--

--