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
-
Types are PascalCase
-
Preprocessor defines are UPPER_CASE, except for commonly used macros like:
#define array_size(a) (sizeof(a) / sizeof(a[0]))#define defer(code) /* stuff */(see A Defer Statement For C++11)
-
Constants are UPPER_CASE
-
Enums types are PascalCase.
- Raw enum values are TypeName_PascalCase
- Enum class values are PascalCase
-
Functions are snake_case
-
Variables are snake_case
- Public member variables are snake_case
- Private member variables are m_snake_case
- Static variables are s_snake_case
- Global variables are g_snake_case
-
File names are either snake_case.cpp or snake_case.h
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
classfollowed by thepublicaccess specifier, but that's exactly whatstructdoes.
// 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, usebool 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);
}
)";