Naming Conventions
- Classes/Structs: PascalCase (MotorController, SensorData)
- Functions/Methods: camelCase (readSensor, calculateSpeed)
- Variables: camelCase (sensorValue, maxSpeed)
- Constants: UPPER_SNAKE_CASE (MAX_BUFFER_SIZE, DEFAULT_TIMEOUT)
- Macros: UPPER_SNAKE_CASE (ENABLE_DEBUG, PIN_LED)
- Namespaces: lowercase (sensors, communication)
- Private members: prefix with m_ (m_temperature, m_isActive)
- Pointers: prefix with p or suffix with Ptr (pBuffer, dataPtr)
- Enums: PascalCase for type, UPPER_SNAKE_CASE for values
enum class State { IDLE, RUNNING, ERROR };
File Organization
- Headers: *.h* extension
- Source: *.cpp* or *.c* extension
- One class per file (except nested/helper classes)
- File naming: match class name (MotorController.h, MotorController.cpp)
- Header guards: use
#pragma once
Header Files
- Include order:
- Corresponding header (in .cpp)
- Project headers
#include "file.h"
- System headers
#include <file.h>
- Forward declarations: use when possible to reduce dependencies
- Minimize includes: only include what you use
- Separate interface from implementation
Code Formatting
- Indentation: 2 spaces (no tabs)
- Braces: Allman style (be consistent)
if (condition)
{
doSomething();
}
- Line length: 80-120 characters maximum
- Spaces: around operators (
a + b, not a+b)
- Pointer/reference: attach to variable (
int *ptr, not int* ptr)
Memory Management
PC
- Prefer RAII: Resource Acquisition Is Initialization
- Smart pointers: use
std::unique_ptr, std::shared_ptr or std::weak_ptr
- Avoid raw new/delete: use smart pointers or containers
- No memory leaks: every allocation must have deallocation
Microcontroller
- Minimize dynamic allocation: prefer stack or static allocation
- No new/delete in ISRs: Interrupt Service Routines
- Pre-allocate buffers: avoid runtime allocation
- Static memory pools: if dynamic allocation needed
- Check heap fragmentation: monitor free heap regularly
Functions
- Single Responsibility: one function, one task
- Function length: maximum 50 lines (20-30 preferred)
- Parameters: maximum 4-5 parameters (use structs if more)
- Pass by reference: for non-primitive types (const Type&)
- Return values: prefer return over out-parameters
- Const correctness: mark const wherever applicable
Classes
- Rule of Five: define or delete copy/move constructors and assignment
- Virtual destructors: for base classes with virtual methods
- Explicit constructors: use explicit for single-argument constructors
class MyClass
{
MyClass() = default;
virtual ~MyClass() = default;
MyClass(const MyClass &other) = delete;
MyClass(MyClass &&other) noexcept = delete;
MyClass &operator=(const MyClass &other) = delete;
MyClass &operator=(MyClass &&other) noexcept = delete;
explicit MyClass(int val);
}
Modern C++ Features (PC)
- Use auto: when type is obvious
- Range-based loops:
for (const auto& item : container)
- nullptr: instead of NULL or 0
- constexpr: for compile-time constants
- Lambda functions: for short callbacks
- Move semantics: implement when appropriate
- std::array: instead of C-style arrays
- enum class: instead of plain enums
- Use C++ structured bingings
std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}};
for (const auto& pair : ages) {
Serial.println(pair.first);
Serial.println(pair.second);
}
for (const auto& [name, age] : ages) {
Serial.println(name);
Serial.println(age);
}
int arr[][2] = {{1, 2}, {3, 4}, {5, 6}};
for (auto& [a, b] : arr) {
cout << a << " " << b << endl;
}
std::vector<std::tuple<int, float, std::string>> data;
for (const auto& [id, value, name] : data) {
}
- User C++ Attributes
[[nodiscard]] int calculateSum(int a, int b);
calculateSum(3, 4);
void debug([[maybe_unused]] int value);
switch (value) {
case 1:
doSomething();
[[fallthrough]];
case 2:
doSomethingElse();
break;
}
[[deprecated("Use newFunction() instead")]]
void oldFunction();
if (condition) [[likely]] {
} else {
}
Embedded / Microcontroller Specific
- Avoid exceptions: often disabled on MCUs (use error codes)
- Avoid RTTI: disable Runtime Type Information (-fno-rtti)
- Minimize STL: use selectively (some implementations are heavy)
- Volatile keyword: for hardware registers and ISR-shared variables
- ISR constraints:
- Keep ISRs short (< 10 microseconds if possible)
- No blocking operations
- Avoid dynamic memory allocation
- Minimize shared variable access
- Optimization pragmas: use carefully
- Inline functions: for time-critical code
- Bit manipulation: use for register operations
PORTB |= (1 << PB5);
PORTB &= ~(1 << PB5);
Constants and Macros
- Prefer const/constexpr over define
- Macros: use UPPERCASE and parentheses
#define MAX(a, b) ((a) > (b) ? (a) : (b))
- Configuration constants: group in header
namespace Config
{
constexpr uint32_t BAUD_RATE = 115200;
constexpr uint8_t BUFFER_SIZE = 64;
}
Error Handling
PC
- Exceptions: for exceptional conditions
- Error codes: for expected failures
- RAII: for resource cleanup
Microcontroller
- Return codes: primary error handling method
- Error enums: define clear error types
enum class ErrorCode { OK, TIMEOUT, INVALID_PARAM, HW_ERROR };
- Assert macros: for development/debugging
- Watchdog timer: implement for fault recovery
Comments and Documentation
- Doxygen format: for API documentation
- Self-documenting code: prefer clear names over comments
- TODO/FIXME: mark incomplete or problematic code
- Explain "why": not "what" (code shows what)
- Update comments: keep in sync with code
Code Quality
- No magic numbers: use named constants
constexpr int MAX_RETRIES = 3;
if (retries > MAX_RETRIES)
{
}
- Avoid global variables: use singletons or dependency injection
- Initialize variables: always initialize at declaration
- Avoid deep nesting: maximum 3-4 levels
- Guard clauses: early return for error conditions
if (!isValid)
return ERROR_INVALID;
Concurrency (where applicable)
- Atomic operations: for shared variables
- Mutexes: protect critical sections (PC)
- Disable interrupts: for critical sections (MCU)
- Volatile: for interrupt-shared variables
- Lock-free algorithms: when possible on MCU
Performance
- Avoid floating-point: on MCUs without FPU
- Fixed-point arithmetic: for fractional values on MCU
- Lookup tables: for complex calculations
- Inline critical functions: use inline keyword
- Profile before optimizing: measure, don't guess
- Compiler optimization flags: -O2, -O3 (test thoroughly)
Testing
- Unit tests: for all public interfaces
- Mock hardware: for embedded testing
- Test on target: always test on actual hardware
- Boundary conditions: test edge cases
- Code coverage: aim for >80%
Version Control
- Meaningful commits: descriptive commit messages
- Feature branches: for new features
- Code review: before merging
- Avoid generated files: in repository (.gitignore)
Build System
- CMake: as soon as possible
- Compiler warnings: enable all (-Wall -Wextra -Wpedantic)
- Treat warnings as errors: -Werror
- Debug/Release configs: separate configurations
—
Lexicon
RTTI (Run-time type information): run-time type information is a feature that exposes information about an object's data type at runtime. RTTI can be used to do safe typecasts using the dynamic_cast<> operator, and to manipulate type information at runtime using the typeid operator and std::type_info class.
RAII (Resource acquisition is initialization): Resource acquisition is initialization is a programming idiom used in several object-oriented, statically typed programming languages to describe a particular language behavior. Resource acquisition (allocation) is done during object creation by the constructor, while resource deallocation (release) is done during object destruction by the destructor. In other words, resource acquisition must succeed for initialization to succeed. The resource is guaranteed to be held between the end of initialization and the starts of finalization, and only as long as the object is active. Thus, if there are no object leaks, there are no resource leaks.