C++ intro - MarekBykowski/readme GitHub Wiki
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.
- RAII — automatic cleanup
- Smart pointers — ownership in the type
- Classes — struct + functions, bound
- References vs pointers
std::string&std::vectorauto& range-for- Lambdas
- STL containers & algorithms
- Move semantics
- Templates — reading level
unionvsvariantvsoptional- Error handling: exceptions &
optional - Threads & synchronization
- Const-correctness
- Gotchas checklist
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 forgetMental model: RAII is your goto cleanup, made automatic and impossible to skip.
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.
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).
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 copyPass big objects by const T& to avoid copies. Use a pointer only when "no
object" (null) is a valid state — otherwise reference.
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.
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 RHSUse auto when the type is obvious or verbose; spell it out when clarity helps.
void on_msg(void (*cb)(void *ctx, Msg *m), void *ctx); // fn ptr + void* ctxqueue.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.
#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.
std::vector<uint8_t> makeBuffer();
auto b = makeBuffer(); // not copied — moved/elided
queue.push(std::move(b)); // transfer ownership; b now emptyYou 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.
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).
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 UBClassic 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 typeAnd 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 neededTrap: 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> |
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.
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.
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 copyC++ 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.
-
Copy vs move: passing a big object by value copies it — use
const T&orstd::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_ifreturnsnullptr. -
optionalis not a pointer:*optdereferences the value, but checkif (opt)first — there's no null. -
Union UB: reading a different member than written is UB; use
memcpy/bit_castfor byte reinterpretation. -
Raw
new/delete: almost never needed — prefermake_unique/make_shared. -
Iterator invalidation: modifying a container (e.g.
push_backthat reallocs) can invalidate iterators/pointers into it — same class of bug as a stale pointer afterrealloc.
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.