C++ Coding Style

UPDATE (October 10, 2023): There's some ideas here that I still stick with, but I don't strictly follow this style anymore.


C++ is a ridiculously complex language. Most people stick to a subset of its features. I've been using a lot of C++ for side projects that never see the light of day, and over time, I've developed a style that I like and I wanted to document it.

Code Format

Use Clang-Format for code formatting:

BasedOnStyle: LLVM
IndentWidth: 2
AllowShortCaseLabelsOnASingleLine: true

Naming

Example:

constexpr const char *SETTINGS_FILE_PATH = "./data/settings.ini";

struct String {
  String() = default;
  String(const char *);

private:
  char *m_buf = nullptr;
  i32 m_size = 0;
  i32 m_capacity = 0;
};

enum EntityKind : i32 {
  EntityKind_None,
  EntityKind_Player,
  EntityKind_Bullet,
  EntityKind_Enemy,
};

enum class Direction : i32 {
  North,
  East,
  South,
  West,
};

struct Entity {
  EntityKind kind;
  Direction direction;
  String name;
};

String g_str;

const char *some_function(i32 kind) {
  static char s_str[2048];
  return s_str;
}

break

If there are brackets for a case statement, the break goes inside.

switch (val) {
case Kind_Foo: {
  i32 n = 0;
  do_stuff(&n);
  printf("%d\n", n);
  break; // <-- here
}
}

Sized Integers

Prefer explicitly sized integers such as int32_t. Use the following type aliases:

using i8 = int8_t;
using i16 = int16_t;
using i32 = int32_t;
using i64 = int64_t;
using u8 = uint8_t;
using u16 = uint16_t;
using u32 = uint32_t;
using u64 = uint64_t;

Exceptions

Never use throw. Never catch exceptions.

Read Exceptions — And Why Odin Will Never Have Them. TL;DR: Errors are not special. Treat errors as values.

Parameter Order

Custom memory allocator comes first, then output parameters, then the rest of the function parameters.

bool make_texture(Allocator *a, Texture *out, u8 *data, i32 w, i32 h);

Constructors

If object creation can fail, use functions instead of constructors.

// bad
// if this can fail. how do you let the user know?
// is there a global error flag? or does it throw an exception?
// remember that we're trying to avoid exceptions
Bullet::Bullet(Entity *owner);

// good
Maybe<Bullet> make_bullet(Entity *owner);

// also good
bool make_bullet(Bullet *out, Entity *owner);

Destructors

Avoid writing destructors. No RAII.

Defer can help with some of the friction that comes with explicit destruction.

Strings

Prefer string views over C-style strings or std::string.

Unlike C strings, string views stores the length. Unlike string buffers (like std::string), substring is constant time and does not require memory allocation. Also, copying a string view is very cheap.

C Header Files

Use <stdio.h>, <math.h>, etc, over their C++ counterparts <cstdio>, <cmath>, etc.

The idea with the C++ headers was probably to avoid polluting the global namespace, but they don't actually do that so there's no benefit.

class

Avoid defining types with class.

Public members should be listed first. It's common to see class followed by the public access specifier, but that's exactly what struct does.

// bad
class Foo {
public:
  Foo();
private:
  i32 m_the_data;
};

// good
struct Foo {
  Foo();
private:
  i32 m_the_data;
};

Public Members

Public members go at the top.

The public interface/API is the most important information to look for. The elements are sorted by importance.

// bad
struct Foo {
private:
  i32 m_the_data;
public:
  Foo();
};

// also bad
class Foo {
  i32 m_the_data;
public:
  Foo();
};

Zero Initialization

Prefer ZII (Zero Is Initialization).

template <typename T> struct Array {
private:
  T *m_buffer = nullptr;
  i32 m_size = 0;
  i32 m_capacity = 0;
};

Easy to reason with struct data if most types are initialized the same way. It's also common to recieve zero initalized memory from custom memory allocators.

The name of a boolean may be flipped to support ZII. For example, instead of bool alive = true, use bool dead = false.

Default Constructor

If any user constructor is provided, declare a default constructor.

struct Shader {
  Shader() = default;
  Shader(const char *filename);

private:
  u32 m_id = 0;
};

mutable

Avoid mutable.

Constant values should be constant.

friend

Avoid friend.

Hidden data should be hidden.

Signed Integers

Prefer signed integers over unsigned.

You can't write a for loop that walks an array backwards with unsigned integers:

for (u32 i = some_unsigned_int; i >= 0; i--) {
  // infinite loop!
}

for (i32 i = some_unsigned_int; i >= 0; i--) {
  // signed/unsigned mismatch
}

Struct Initialization

Zero initialize structs and C style arrays with = {}.

Image m_white = {};
Value m_stack[STACK_MAX] = {};

Header guards

Header files should use #pragma once.

Supported by commonly used compilers. Less typing compared to header guards.

Type Casting

Prefer C style casts instead of static_cast, dynamic_cast, ...

Most type casts involve casting between integers or from void *. For these cases, C++ casts just adds extra keystrokes for little benefit.

typename

Use typename for declaring template types.

template <class T> struct Array; // bad
template <typename T> struct Array; // good

Sized Enums

Set an explicit size for enums.

This is so that the enum type can be stored in a struct with a known size.

enum EnemyState : i32 {
  EnemyState_Idle,
  EnemyState_Alert,
  EnemyState_Chase,
  EnemyState_Dead,
};

References

Prefer passing paremeters by pointer rather than by mutable reference. References are okay if its const.

It's easier to see that something can be changed when & is involved at the call site.

// does do_stuff get the data as a copy? const reference?
// mutable reference? who knows?
do_stuff(the_data);

// oh okay, the_data is likely mutated after calling do_stuff
do_stuff(&the_data);

Header Include Order

When include order matters, add an empty line in between the includes.

The rationale is that Clang-Format will reorder includes if they're grouped together.

#include "texture.h"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

void *

Avoid void *.

There's usually some meaningful type that can be used with the pointer. When dealing with a raw memory block, then the type can be u8 *, for pointer arithmetic.

Conditions

The condition in an if statement should be a boolean type. No truthy/falsy expressions.

char *buf = get_buf_data();
if (buf) {} // bad
if (buf != nullptr) {} // good

Multi-line Strings

Use raw string literals for multi-line strings.

auto fragment = R"(
  #version 330 core

  in vec2 v_texindex;
  out vec4 f_color;
  uniform sampler2D u_texture;

  void main() {
    f_color = texture(u_texture, v_texindex);
  }
)";