VertexNova Coding Guidelines
Version: 1.0
Last Updated: January 2026
C++ Standard: C++17 minimum, C++20 preferred
Introduction
These coding guidelines establish standards for writing C++ and Objective-C++ code in the VertexNova engine project. The guidelines prioritize:
- Clarity and Readability: Code should be self-documenting and easy to understand
- Modern C++: Leverage C++17/20 features for safety, performance, and expressiveness
- Consistency: Uniform style across the entire codebase
- Maintainability: Code that can be easily modified and extended
- Cross-Platform Compatibility: Code that works across Windows, macOS, Linux, iOS, and Web
These guidelines draw inspiration from:
- Google C++ Style Guide
- Epic C++ Coding Standard
- C++ Core Guidelines
- NVIDIA Carbonite SDK Coding Style
General Principles
Philosophy
- Prefer clarity over cleverness: Write code that is obvious, not clever
- Make invalid states unrepresentable: Use the type system to prevent errors
- Prefer compile-time checks over runtime checks: Use
constexpr,static_assert, and strong typing - Follow the Rule of Zero/Five: Let the compiler generate special member functions when possible
- Use RAII: Resource management through object lifetime
- Prefer composition over inheritance: Favor composition and interfaces over deep inheritance hierarchies
Code Organization
- One class per header file: Each class should have its own header file
- Logical grouping: Related classes should be in the same directory/namespace
- Minimal dependencies: Include only what you need, forward declare when possible
- Clear module boundaries: Use namespaces to organize code logically
Naming Conventions
Summary Table
| Construct | Style | Example |
|---|---|---|
| Classes/Structs | PascalCase | Buffer, ShaderCompiler |
| Interface classes | I + PascalCase | IRenderer, IBuffer |
| Enums | PascalCase | LogSink, ShaderStage |
| Enum values | e + PascalCase + explicit value | eNone = 0, eConsole = 1 |
| Type aliases | PascalCase | EntityId, BufferHandle |
| Functions/Methods | camelCase | initialize(), createBuffer() |
| Constants | k + PascalCase | kMaxBufferSize |
| Private/Protected members | snake_case + _ | buffer_size_, is_initialized_ |
| Public members | snake_case | buffer_size, is_initialized |
| Local variables | snake_case | buffer_size, file_path |
| Function parameters | snake_case | buffer_size, usage |
| Static members (private) | s_ + snake_case + _ | s_instance_count_ |
| Static members (public) | s_ + snake_case | s_instance_count |
| Global variables | g_ + snake_case | g_instance, g_config |
| Booleans | is_, has_, can_, should_ prefix | is_ready_, has_alpha_ |
| Macros | ALL_CAPS | VNE_ASSERT, VNE_PLATFORM_WINDOWS |
| Namespaces | lowercase | vne, xgl, xwin |
| File names | snake_case | shader_compiler.h |
| Header guards | #pragma once | #pragma once |
General Rules
- Be descriptive: Names should clearly indicate purpose
- Avoid abbreviations: Use full words unless the abbreviation is widely understood (e.g.,
id,max,min) - Consistency: Use the same naming style throughout the codebase
Types (Classes, Structs, Type Aliases)
Use PascalCase for all type names:
// Classes
class ShaderCompiler { };
class MetalSurface { };
class LogManager { };
// Structs
struct LoggerConfig { };
struct SurfaceDescriptor { };
struct ValidationResult { };
// Type aliases (no T prefix)
using BufferHandle = uint32_t;
using EntityId = uint64_t;
using DeviceId = std::string;
Interface Classes
Use I prefix followed by PascalCase for abstract interface classes:
// Interface classes
class IRenderer {
public:
virtual ~IRenderer() = default;
virtual void render() = 0;
virtual void present() = 0;
};
class IBuffer {
public:
virtual ~IBuffer() = default;
virtual void* map() = 0;
virtual void unmap() = 0;
};
class IInputHandler {
public:
virtual ~IInputHandler() = default;
virtual void onKeyPressed(int key_code) = 0;
virtual void onMouseMoved(float x, float y) = 0;
};
Enums
Use PascalCase for enum names. Use e prefix with PascalCase for enum values, and always specify explicit values:
// Enum class with e-prefixed values and explicit values
enum class LogSink {
eNone = 0,
eConsole = 1,
eFile = 2,
eBoth = 3
};
enum class ShaderStage {
eVertex = 0,
eFragment = 1,
eCompute = 2,
eGeometry = 3,
eTessControl = 4,
eTessEvaluation = 5
};
enum class BufferUsage {
eNone = 0,
eVertex = 1 << 0,
eIndex = 1 << 1,
eUniform = 1 << 2,
eStorage = 1 << 3,
eTransferSrc = 1 << 4,
eTransferDst = 1 << 5
};
Functions and Methods
Use camelCase for all functions and methods:
class Renderer {
public:
void initialize();
bool createBuffer(size_t size);
void setViewport(int width, int height);
void submitCommandBuffer();
// Getters use camelCase too
int getWidth() const;
bool isInitialized() const;
private:
void initializeInternal();
bool validateState();
void cleanupResources();
};
// Free functions
void processVertices(const std::vector<Vertex>& vertices);
std::vector<uint32_t> compileShader(const std::string& source);
Variables
Use snake_case for all variables:
// Local variables
int window_width = 1920;
int window_height = 1080;
std::string shader_source;
std::vector<uint32_t> spirv_binary;
// Function parameters
void setViewport(int width, int height, float min_depth, float max_depth);
Member Variables
Use snake_case with conventions based on visibility:
class Buffer {
public:
// Public members: snake_case (no trailing underscore)
size_t size;
BufferUsage usage;
bool is_mapped;
protected:
// Protected members: snake_case with trailing underscore
uint32_t handle_;
Device* device_;
private:
// Private members: snake_case with trailing underscore
void* data_;
bool is_initialized_;
size_t capacity_;
};
Boolean Variables
Use descriptive prefixes (is_, has_, can_, should_):
class Texture {
private:
bool is_loaded_;
bool has_alpha_;
bool can_resize_;
bool should_generate_mipmaps_;
};
// Local booleans
bool is_valid = validateInput(data);
bool has_error = result.error_code != 0;
Static Member Variables
Use s_ prefix with snake_case:
class Renderer {
public:
// Public static: s_ prefix, no trailing underscore
static int s_instance_count;
static std::string s_default_shader_path;
private:
// Private static: s_ prefix with trailing underscore
static Device* s_device_;
static bool s_is_initialized_;
};
Global Variables
Use g_ prefix with snake_case:
// Global variables (use sparingly)
extern Device* g_device;
extern Config g_config;
extern bool g_is_debug_mode;
Constants
Use k prefix followed by PascalCase for compile-time constants.
For file-scope constants, place them in an anonymous namespace (preferred over static):
// File-scope constants in anonymous namespace
namespace {
constexpr int kMaxBufferSize = 1024 * 1024;
constexpr float kPi = 3.14159265359f;
constexpr const char* kDefaultShaderPath = "shaders/default.glsl";
} // namespace
// Class constants (use static constexpr)
class Renderer {
public:
static constexpr int kMaxTextures = 16;
static constexpr float kDefaultFov = 60.0f;
static constexpr size_t kMinBufferAlignment = 256;
};
Namespaces
Use lowercase for namespace names:
namespace vne {
namespace xgl {
// Graphics API code
class Device { };
class Buffer { };
}
namespace xwin {
// Window/Platform code
class Window { };
class Surface { };
}
namespace log {
// Logging code
class Logger { };
}
}
// C++17 nested namespace syntax
namespace vne::xgl::backend {
class VulkanDevice { };
class MetalDevice { };
}
Macros
Use ALL_CAPS with project prefix:
#define VNE_ASSERT(condition) /* ... */
#define VNE_UNUSED(variable) ((void)(variable))
#define VNE_DISABLE_COPY(ClassName) \
ClassName(const ClassName&) = delete; \
ClassName& operator=(const ClassName&) = delete
#define VNE_PLATFORM_WINDOWS 1
#define VNE_PLATFORM_MACOS 2
#define VNE_PLATFORM_LINUX 3
File Names
Use snake_case for source files:
shader_compiler.h
shader_compiler.cpp
metal_surface.h
metal_surface.mm
uikit_window_manager.h
uikit_window_manager.mm
Code Formatting
Indentation and Spacing
- 4 spaces per indentation level (no tabs)
- 120 characters maximum line length
- Trailing whitespace: Remove all trailing whitespace
- End of line: Use LF (
\n) for all platforms
Braces
Use attached braces (opening brace on same line):
if (condition) {
// code
}
class MyClass {
public:
void method() {
// code
}
};
Exception: Function definitions with long return types can use newline:
template<typename T>
std::vector<T>
MyClass::processData(const std::vector<T>& input) {
// code
}
Spacing
// Around operators
int result = a + b;
bool is_valid = (x > 0) && (y < 100);
// After keywords
if (condition) { }
for (int i = 0; i < count; ++i) { }
while (condition) { }
// In function calls
function(arg1, arg2, arg3);
// In template declarations
template<typename T, typename U>
void function(T t, U u);
Pointer and Reference Alignment
Use left-aligned (attached to type):
int* ptr; // Pointer attached to type
int& ref; // Reference attached to type
const char* str; // Const pointer
const int& value; // Const reference
Header Files
Header Guards
Use #pragma once for header guards:
#pragma once
// Header content here
Include Order
Order includes in groups, separated by blank lines:
- Corresponding header file (for
.cppfiles) - Project headers (
"vertexnova/...") - System headers (
<...>) - Third-party headers
// shader_compiler.cpp
#include "shader_compiler.h" // 1. Corresponding header
#include "vertexnova/logging/logging.h" // 2. Project headers
#include "vertexnova/graphics/xgl/types.h"
#include <vector> // 3. System headers
#include <string>
#include <memory>
#include <shaderc/shaderc.hpp> // 4. Third-party headers
Header File Structure
#pragma once
/* ---------------------------------------------------------------------
* Copyright (c) 2025 Ajeet Singh Yadav. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License")
*
* Author: Ajeet Singh Yadav
* Created: January 2026
* ----------------------------------------------------------------------
*/
// 1. Includes (in order: project, system, third-party)
#include "vertexnova/graphics/xgl/types.h"
#include <vector>
#include <string>
#include <memory>
// 2. Forward declarations
namespace vne::xgl {
class Device;
struct SurfaceDescriptor;
}
// 3. Namespace
namespace vne::xgl {
// 4. Class/struct/enum definitions
class MetalSurface {
public:
MetalSurface();
~MetalSurface();
// Rule of Five
MetalSurface(const MetalSurface&) = delete;
MetalSurface& operator=(const MetalSurface&) = delete;
MetalSurface(MetalSurface&&) noexcept;
MetalSurface& operator=(MetalSurface&&) noexcept;
bool initialize(const SurfaceDescriptor& desc);
void cleanup();
private:
bool is_initialized_;
SurfaceDescriptor descriptor_;
};
} // namespace vne::xgl
Forward Declarations
Prefer forward declarations over includes when possible:
// In header file
class Device; // Forward declaration
struct SurfaceDescriptor; // Forward declaration
// Include only when necessary
#include <vector> // Need std::vector
#include <string> // Need std::string
Classes and Structs
Class Organization
Order of declarations:
- Public types and constants
- Public constructors and destructor
- Public methods
- Protected members (if any)
- Private members
class Buffer {
public:
// 1. Types and constants
static constexpr size_t kMaxSize = 1024 * 1024;
// 2. Constructors and destructor
Buffer();
explicit Buffer(size_t size);
~Buffer();
// Rule of Five
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
Buffer(Buffer&&) noexcept;
Buffer& operator=(Buffer&&) noexcept;
// 3. Public methods
bool initialize(size_t size);
void* map();
void unmap();
size_t getSize() const { return size_; }
protected:
// 4. Protected members (if any)
private:
// 5. Private members
uint32_t handle_;
size_t size_;
bool is_mapped_;
};
Structs vs Classes
- Structs: Use for data containers, POD types, simple aggregates
- Classes: Use for types with invariants, encapsulation, behavior
// Struct: Simple data container
struct Vertex {
float position[3];
float normal[3];
float tex_coord[2];
};
// Class: Has behavior and invariants
class Buffer {
public:
bool initialize(size_t size); // Enforces invariants
void* map(); // Behavior
private:
uint32_t handle_; // Encapsulation
};
Rule of Zero/Five
Prefer Rule of Zero when possible:
class SimpleContainer {
std::vector<int> data_; // Compiler-generated copy/move is fine
};
// Explicitly delete when needed
class NonCopyable {
public:
NonCopyable() = default;
~NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
NonCopyable(NonCopyable&&) = default;
NonCopyable& operator=(NonCopyable&&) = default;
};
Constructors
Use explicit constructors to prevent implicit conversions:
class Buffer {
public:
explicit Buffer(size_t size); // Prevents implicit conversion
};
// Buffer b = 1024; // Compile error
// Buffer b(1024); // OK
Use member initializer lists:
class Renderer {
public:
Renderer(Device* device, int width, int height)
: device_(device)
, width_(width)
, height_(height)
, is_initialized_(false) {
// Constructor body
}
private:
Device* device_;
int width_;
int height_;
bool is_initialized_;
};
Functions
Function Signatures
Use clear parameter names in declarations:
// Good: Clear parameter names
bool createBuffer(size_t size, BufferUsage usage, Buffer** out_buffer);
// Avoid: Unclear
bool createBuffer(size_t, BufferUsage, Buffer**);
Prefer references over pointers for non-nullable parameters:
// Reference for non-nullable
void processData(const std::vector<int>& data);
// Pointer for nullable
void setCallback(Callback* callback); // nullptr means no callback
Return Values
Prefer return values over output parameters:
// Preferred: Return value
std::vector<uint32_t> compileShader(const std::string& source);
// Avoid: Output parameter
void compileShader(const std::string& source, std::vector<uint32_t>* out);
Use [[nodiscard]] for functions whose return values should not be ignored:
[[nodiscard]] bool initialize();
[[nodiscard]] std::unique_ptr<Buffer> createBuffer();
Const Correctness
Mark methods const when they don't modify object state:
class Buffer {
public:
size_t getSize() const { return size_; }
bool isMapped() const { return is_mapped_; }
void* map() { /* modifies state */ } // Not const
};
Noexcept
Mark functions noexcept when they don't throw:
class Buffer {
public:
size_t getSize() const noexcept { return size_; }
Buffer(Buffer&& other) noexcept;
};
Memory Management
Smart Pointers
Prefer smart pointers over raw pointers:
// Use std::unique_ptr for exclusive ownership
std::unique_ptr<Buffer> createBuffer() {
return std::make_unique<Buffer>(size);
}
// Use std::shared_ptr for shared ownership
std::shared_ptr<Texture> texture = Texture::create("texture.png");
// Use std::weak_ptr to break circular references
class Node {
private:
std::weak_ptr<Node> parent_;
std::vector<std::shared_ptr<Node>> children_;
};
RAII
Use RAII for resource management:
class FileHandle {
public:
explicit FileHandle(const std::string& path)
: file_(fopen(path.c_str(), "r")) {
if (!file_) {
throw std::runtime_error("Failed to open file");
}
}
~FileHandle() {
if (file_) {
fclose(file_);
}
}
FILE* get() { return file_; }
private:
FILE* file_;
};
Arrays and Vectors
Prefer std::vector over C arrays:
// Preferred
std::vector<int> indices;
std::vector<Vertex> vertices;
// Use std::array for fixed-size arrays
std::array<float, 3> position;
std::array<float, 4> color;
Modern C++ Features
Auto
Use auto when the type is obvious from context:
// Good: Type is obvious
auto buffer = createBuffer();
auto count = items.size();
auto it = map.find(key);
// Prefer explicit type when clarity matters
std::unique_ptr<Buffer> buffer = createBuffer();
Range-Based For Loops
Prefer range-based for loops:
// Preferred
for (const auto& item : items) {
processItem(item);
}
// Modify in place
for (auto& item : items) {
item.update();
}
Lambda Expressions
Use lambdas for short, local functions:
std::sort(items.begin(), items.end(),
[](const auto& a, const auto& b) { return a.value < b.value; });
auto processor = [&data](int index) {
processItem(data[index]);
};
Constexpr
Use constexpr for compile-time constants and functions:
constexpr int kMaxBuffers = 256;
constexpr float kPi = 3.14159265359f;
constexpr int square(int x) {
return x * x;
}
constexpr int squared = square(5); // Computed at compile time
If Constexpr
Use if constexpr for compile-time conditionals:
template<typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
processInteger(value);
} else {
processOther(value);
}
}
Structured Bindings
Use structured bindings for tuples and pairs:
auto [it, inserted] = map.insert({key, value});
if (inserted) {
// New element was inserted
}
for (const auto& [key, value] : map) {
processPair(key, value);
}
Strongly-Typed Enums
Always use enum class instead of plain enum:
// Preferred
enum class LogLevel {
eTrace = 0,
eDebug = 1,
eInfo = 2,
eWarn = 3,
eError = 4,
eFatal = 5
};
nullptr
Always use nullptr instead of NULL or 0:
Device* device = nullptr;
if (device == nullptr) { }
Templates
Template Parameters
Use descriptive names for template parameters:
template<typename ElementType, size_t ArraySize>
class FixedArray { };
// T is acceptable for simple, generic templates
template<typename T>
T clamp(T value, T min_val, T max_val);
Concepts (C++20)
Use concepts to constrain templates:
template<std::integral T>
T square(T value) {
return value * value;
}
template<typename T>
concept Drawable = requires(T t) {
t.draw();
};
template<Drawable T>
void render(const T& object) {
object.draw();
}
Objective-C++ Guidelines
File Organization
Separate Objective-C and C++ code when possible:
// metal_surface.h (C++ header)
#pragma once
#include "vertexnova/graphics/xgl/types.h"
namespace vne::xgl {
class MetalSurface {
public:
bool initialize(const SurfaceDescriptor& desc);
};
}
// metal_surface.mm (Objective-C++ implementation)
#include "metal_surface.h"
#import <Metal/Metal.h>
namespace vne::xgl {
bool MetalSurface::initialize(const SurfaceDescriptor& desc) {
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
// ...
}
}
Include/Import Order
Order includes/imports:
- C++ headers (
#include) - Objective-C headers (
#import) - System frameworks
// metal_surface.mm
#include "metal_surface.h" // 1. C++ headers
#include "vertexnova/logging/logging.h"
#import <Metal/Metal.h> // 2. Objective-C headers
#import <QuartzCore/CAMetalLayer.h>
#ifdef VNE_PLATFORM_MACOS
#import <Cocoa/Cocoa.h> // 3. System frameworks
#elif defined(VNE_PLATFORM_IOS)
#import <UIKit/UIKit.h>
#endif
Memory Management
Use ARC (Automatic Reference Counting):
- ARC is enabled by default in modern Objective-C++
- Use
__strong,__weak,__unsafe_unretainedwhen needed - Avoid manual
retain/releasecalls
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
CAMetalLayer* layer = [[CAMetalLayer alloc] init];
// Weak reference to break cycles
__weak id<MTLDevice> weak_device = device;
Naming Conventions
Follow Objective-C naming conventions for Objective-C code, C++ conventions for C++ code:
// Objective-C style
@interface MetalSurfaceWrapper : NSObject
- (BOOL)initializeWithDescriptor:(SurfaceDescriptor*)descriptor;
- (void)cleanup;
@property (nonatomic, strong) CAMetalLayer* layer;
@end
// C++ style for C++ code
namespace vne::xgl {
class MetalSurface {
public:
bool initialize(const SurfaceDescriptor& desc);
};
}
Error Handling
Exceptions
Use exceptions for error handling:
class Buffer {
public:
static std::unique_ptr<Buffer> create(size_t size) {
if (size == 0) {
throw std::invalid_argument("Buffer size must be > 0");
}
auto buffer = std::make_unique<Buffer>();
if (!buffer->initialize(size)) {
throw std::runtime_error("Failed to initialize buffer");
}
return buffer;
}
};
Error Codes
Use error codes when performance is critical or interfacing with C APIs:
enum class Result {
eSuccess = 0,
eInvalidParameter = 1,
eOutOfMemory = 2,
eDeviceLost = 3
};
Result createBuffer(size_t size, Buffer** out_buffer);
Assertions
Use assertions for programming errors (bugs), not user errors:
#include <cassert>
void setViewport(int width, int height) {
assert(width > 0 && height > 0); // Programming error if false
// ...
}
Best Practices
Performance
- Measure before optimizing: Profile first, optimize second
- Avoid premature optimization: Write clear code first
- Use
constandconstexpr: Enable compiler optimizations - Prefer stack allocation: Use stack when possible
- Cache-friendly data structures: Consider memory layout
Safety
- Use strong types: Avoid primitive obsession
- Validate inputs: Check parameters at boundaries
- Use RAII: Automatic resource management
- Prefer const: Immutability reduces bugs
- Avoid undefined behavior: Know your language
Testing
- Write testable code: Small, focused functions
- Use dependency injection: Test with mocks
- Test edge cases: Boundary conditions, null inputs
- Keep tests fast: Unit tests should run quickly
Documentation
Use Doxygen-style comments for public APIs:
/**
* @brief Compiles GLSL source code to SPIR-V binary
*
* This function compiles the provided GLSL source code to SPIR-V format,
* which can be used across multiple graphics backends (Vulkan, Metal, etc.).
*
* @param source GLSL source code to compile
* @param stage Shader stage (vertex, fragment, compute, etc.)
* @param entry_point Entry point function name (default: "main")
* @return SPIR-V binary data as vector of uint32_t, empty on failure
*
* @note This function is thread-safe
*/
std::vector<uint32_t> compileGLSLToSPIRV(
const std::string& source,
ShaderStage stage,
const std::string& entry_point = "main");
Code Review Checklist
General
- Code is formatted with
.clang-format - No compiler warnings
- Code compiles on all target platforms
Naming
- Classes/Structs use PascalCase
- Functions/Methods use camelCase
- Variables use snake_case
- Private members have trailing underscore
- Constants use kPascalCase prefix
- Enums values use ePascalCase prefix with explicit values
Modern C++
- Uses C++17/20 features appropriately
- Smart pointers used instead of raw pointers (where applicable)
constcorrectness appliednoexceptspecified where appropriate- Rule of Zero/Five followed
Safety
- No memory leaks
- No undefined behavior
- Input validation where necessary
- Error handling implemented
Documentation
- Public APIs documented
- Complex logic explained
- Examples provided for non-obvious APIs
Tools and Automation
Formatting
Use .clang-format for automatic formatting:
# Format a file
clang-format -i path/to/file.cpp
# Format all C++ files
find . -name "*.cpp" -o -name "*.h" | xargs clang-format -i
Linting
Use clang-tidy for static analysis:
clang-tidy file.cpp -- -std=c++17
Conclusion
These guidelines are living documents and should evolve with the project. When in doubt:
- Be consistent with existing code
- Prioritize clarity over cleverness
- Follow modern C++ best practices
- Ask for review when uncertain
References: