Skip to content

C intro

MarekBykowski edited this page Aug 8, 2026 · 2 revisions

Modern C++ Cheatsheet for a Kernel Engineer

Companion to the C++ ramp-up plan. Every topic shown C way → C++ way, because you already think in C. Target: reading and modifying a modern C++14/17 codebase like luxonis/depthai-core.

Contents

  1. RAII — automatic cleanup
  2. Smart pointers — ownership in the type
  3. Classes — struct + functions, bound
  4. References vs pointers
  5. std::string & std::vector
  6. auto & range-for
  7. Lambdas
  8. STL containers & algorithms
  9. Move semantics
  10. Templates — reading level
  11. union vs variant vs optional
  12. Error handling: exceptions & optional
  13. Threads & synchronization
  14. Const-correctness
  15. Gotchas checklist

1. RAII — automatic cleanup

Start here; everything else is RAII applied. A resource lives as long as an object; the destructor frees it when scope ends.

// C: manual, every early return must free
FILE *f = fopen("cfg", "r");
if (!f) return -1;
char *buf = malloc(1024);
if (!buf) { fclose(f); return -1; }   // easy to forget
free(buf); fclose(f);
// C++: destructors free automatically, even on exception/early return
{
    std::ifstream f("cfg");
    std::vector<char> buf(1024);
}   // buf freed, f closed here — no cleanup path to forget

Mental model: RAII is your goto cleanup, made automatic and impossible to skip.


2. Smart pointers — ownership in the type

struct node *n = malloc(sizeof *n);   // who owns/frees this? unclear
free(n);
#include <memory>
auto n   = std::make_unique<Node>();     // exclusive owner, zero overhead
auto dev = std::make_shared<Device>();   // shared, reference-counted
std::weak_ptr<Device> w = dev;           // observe without owning (breaks cycles)

Prefer unique_ptr; use shared_ptr only when ownership is genuinely shared. new/delete basically disappear.


3. Classes — struct + functions, bound

struct ring { uint8_t *buf; size_t head, tail, cap; };
void ring_init(struct ring *r, size_t cap);
int  ring_push(struct ring *r, uint8_t v);
void ring_free(struct ring *r);
class Ring {
public:
    explicit Ring(size_t cap) : buf_(cap) {}   // ctor = ring_init
    bool push(uint8_t v);                       // method = ring_push
    // dtor implicit — vector frees itself (no ring_free)
private:
    std::vector<uint8_t> buf_;
    size_t head_ = 0, tail_ = 0;
};

explicit blocks silent conversions; trailing _ marks members (depthai does similar).


4. References vs pointers

void scale(struct frame *f, int factor);   // f may be NULL; use ->
void scale(Frame &f, int factor);     // never null, use .
void inspect(const Frame &f);          // read-only, no copy

Pass big objects by const T& to avoid copies. Use a pointer only when "no object" (null) is a valid state — otherwise reference.


5. std::string & std::vector

char *msg = malloc(len + 1); strcpy(msg, src); /* free(msg) */
std::string msg = src;                 // grows/copies/frees itself
std::vector<uint8_t> payload(len);     // dynamic array, RAII
payload.push_back(0x42);

vector is your malloc'd array with automatic growth and cleanup; .data() gives the raw pointer when you need to hand it to C APIs.


6. auto & range-for

for (size_t i = 0; i < n; i++) process(items[i]);
for (const auto &item : items) process(item);   // no index bugs
auto dev = std::make_unique<Device>();           // type obvious from RHS

Use auto when the type is obvious or verbose; spell it out when clarity helps.


7. Lambdas

void on_msg(void (*cb)(void *ctx, Msg *m), void *ctx);   // fn ptr + void* ctx
queue.setCallback([&](const Message &m) {   // captures context inline
    handle(m, localState);
});

[&] capture by reference, [=] by value, [x] capture just x. depthai uses lambdas for inter-node message handling.


8. STL containers & algorithms

#include <unordered_map>
#include <algorithm>
std::unordered_map<std::string,int> counts;   // hash map, no hand-rolled buckets
counts["frames"]++;

auto it = std::find(v.begin(), v.end(), target);
std::sort(v.begin(), v.end());
int total = std::accumulate(v.begin(), v.end(), 0);

Rule: if you're about to hand-roll a list/map/search, there's an STL container or <algorithm> for it.


9. Move semantics

std::vector<uint8_t> makeBuffer();
auto b = makeBuffer();          // not copied — moved/elided
queue.push(std::move(b));       // transfer ownership; b now empty

You don't need to write move constructors yet, but you must recognize when an API moves vs copies. Think of std::move as "hand off the pointer and null your copy" — a formalized version of what you do manually in C.


10. Templates — reading level

template <typename T>
T clamp(T v, T lo, T hi) { return v < lo ? lo : (v > hi ? hi : v); }

std::vector<int> a;            // vector is itself a template
std::shared_ptr<Device> d;

Be able to read template<typename T> and templated containers. Defer writing advanced templates (metaprogramming, SFINAE, concepts).


11. union vs variant vs optional

A union's members are different types sharing one memory slot — only one is valid at a time (so no, they need not be the same type). Raw union is untagged: it doesn't remember which member is active, and reading the wrong one is undefined behavior.

union Value { int i; float f; char bytes[4]; };
union Value v;
v.i = 42;      // 'i' active
v.f = 3.14f;   // 'f' active — reading v.i now is UB

Classic C fix — a tagged union you manage by hand:

enum Kind { INT, FLOAT, STR };
struct Tagged { enum Kind kind; union { int i; float f; char *s; } as; };

Modern C++ builds that tag + safety in:

#include <variant>
std::variant<int, float, std::string> v;   // tagged, type-safe union
v = std::string("hello");                   // lifetime handled automatically
if (auto p = std::get_if<int>(&v)) use(*p); // safe access, nullptr if wrong type
std::visit([](auto &&x){ handle(x); }, v);  // dispatch on active type

And for the common "value or nothing" case:

#include <optional>
std::optional<Config> loadConfig();   // a Config, or nothing
auto cfg = loadConfig();
if (cfg) use(*cfg);                   // no -1/NULL sentinel needed

Trap: a raw union with a non-trivial member (std::string, std::vector) needs hand-written placement-new + explicit destructor — rarely worth it, use variant. For byte reinterpretation use std::memcpy / (C++20) std::bit_cast, not a union.

Need Use
Same memory, different types, full manual control union
One of several known types, safely std::variant<A,B,C>
A value or nothing std::optional<T>

12. Error handling: exceptions & optional

int rc = do_thing();          // C: return code / errno
if (rc < 0) { /* handle */ }
// C++ style 1: exceptions for exceptional failures
try {
    auto dev = Device::open();   // throws on failure
} catch (const std::runtime_error &e) {
    log(e.what());
}

// C++ style 2: optional/expected for "absence is normal"
std::optional<Config> cfg = loadConfig();
if (!cfg) useDefault();

For now: understand and catch exceptions; don't design around them yet. Use optional where "no value" is an ordinary outcome, not an error.


13. Threads & synchronization

You know the concepts from the kernel (spinlocks, barriers, atomics); here's the C++ API.

#include <thread>
#include <mutex>
#include <atomic>

std::mutex m;
std::atomic<bool> running{true};

std::thread worker([&] {
    while (running) {
        std::lock_guard<std::mutex> lk(m);   // RAII lock — unlocks on scope exit
        // critical section
    }
});
running = false;
worker.join();

lock_guard is RAII again — unlocks in its destructor, so you can't forget. depthai is concurrent by design (device streams), so this shows up early.


14. Const-correctness

void inspect(const Frame &f);      // won't modify f — compiler enforces
int  size() const;                 // method promises not to mutate the object
const auto &ref = getVector();     // read-only view, no copy

C++ pushes const much further than C. Mark methods const when they don't mutate, take const T& for read-only params. It's documentation the compiler checks — lean into it.


15. Gotchas checklist

  • Copy vs move: passing a big object by value copies it — use const T& or std::move.
  • Dangling references: don't return a reference/pointer to a local; don't capture a local by reference in a lambda that outlives it.
  • std::get<T> throws on the wrong variant type; get_if returns nullptr.
  • optional is not a pointer: *opt dereferences the value, but check if (opt) first — there's no null.
  • Union UB: reading a different member than written is UB; use memcpy / bit_cast for byte reinterpretation.
  • Raw new/delete: almost never needed — prefer make_unique/make_shared.
  • Iterator invalidation: modifying a container (e.g. push_back that reallocs) can invalidate iterators/pointers into it — same class of bug as a stale pointer after realloc.

Self-check

Open a random file in depthai-core/src/. Can you follow who owns what memory and where objects are constructed/destroyed? If yes, you're reading C++. If not, revisit sections 1–2 (RAII, smart pointers) — everything else builds on them.

Clone this wiki locally