-
Notifications
You must be signed in to change notification settings - Fork 0
C intro
Companion note to the C++ ramp-up. Covers "one value out of several types" — a place where C habits (
union) and modern C++ (variant/optional) diverge sharply. Shown C way → C++ way.
- A
union's members are different types sharing the same memory — only one is valid at a time. So no, they don't have to be the same type. - Raw
unionis untagged: it does not remember which member is active — you track that yourself, and reading the wrong one is undefined behavior. - In modern C++ you almost always use
std::variant(a tagged, type-safe union) orstd::optional(value-or-nothing) instead of a rawunion.
union Value {
int i;
float f;
char bytes[4];
};
union Value v;
v.i = 42; // 'i' is the active member now
v.f = 3.14f; // 'f' is active now — reading v.i here is undefined behavior- All members overlap in memory;
sizeof(union Value)== size of the largest member. - The union does not store which member is live. Writing one and reading another (type punning) is undefined behavior in C++ (with a narrow exception for a common initial sequence in standard-layout structs). C is more lenient in practice but it's still fragile.
enum Kind { INT, FLOAT, STR };
struct Tagged {
enum Kind kind; // the tag: which member is valid
union {
int i;
float f;
char *s;
} as;
};
struct Tagged t = { .kind = INT, .as.i = 42 };
if (t.kind == INT) use(t.as.i); // you must check the tag yourselfstd::variant is exactly this pattern — the tag and the safety are built in.
If any member has a non-trivial constructor/destructor (e.g. std::string,
std::vector), the compiler can't know which one to destroy. You must manage the
lifetime by hand with placement new and an explicit destructor call:
union U {
int i;
std::string s; // non-trivial: has ctor/dtor
U() {} // must be user-provided
~U() {} // must be user-provided — but which member to destroy??
};
U u;
new (&u.s) std::string("hi"); // placement new to construct
u.s.~basic_string(); // explicit destructor before reuseThis is error-prone and rarely worth it. Which is why you reach for the standard types below instead.
#include <variant>
std::variant<int, float, std::string> v; // holds exactly one of these
v = 42; // now holds int
v = std::string("hello"); // now holds string; lifetime handled
// Safe access #1: get_if returns a pointer, or nullptr if wrong type
if (auto p = std::get_if<int>(&v)) {
use(*p);
}
// Safe access #2: index() tells you which alternative is active (0-based)
if (v.index() == 2) { /* it's the std::string */ }
// Safe access #3: visit dispatches on the active type
std::visit([](auto &&x){ handle(x); }, v);
// get<> throws std::bad_variant_access if you ask for the wrong type
std::string s = std::get<std::string>(v);- Remembers the active type (that's the "tagged" part) and constructs/destroys members correctly — no manual lifetime code.
-
std::visitis the idiomatic way to act on "whatever is currently inside".
Very often you don't need N types — you need "a T, or nothing". In C that's the
-1 / NULL / out-param sentinel pattern. In C++:
#include <optional>
std::optional<Config> loadConfig(); // returns a Config, or nothing
auto cfg = loadConfig();
if (cfg) { // has a value?
use(*cfg); // dereference to get it
}
use(cfg.value_or(Config{})); // value, or a fallbackNo sentinel value to reserve, no separate "success" flag, no out-parameter.
| Need | Use | C analogue |
|---|---|---|
| Same memory, different types, one at a time, full manual control | union |
union |
| One of several known types, safely, lifetime handled | std::variant<A,B,C> |
hand-written tagged union |
| A value or nothing | std::optional<T> |
return -1/NULL, out-param |
| Any type at all (rare, has overhead) | std::any |
void* + manual tag |
depthai note: in a modern C++14/17 codebase like depthai-core you'll see
std::optional and std::variant far more than raw union. Raw union still
shows up in low-level/interop/serialization code — exactly the seam where your
kernel/BSP background meets host-side C++.
- Raw
uniondoes not track the active member — you do (use a tag, or prefervariant). - Reading a different member than the one written is UB in C++ (type punning).
For byte-level reinterpretation use
std::memcpyor (C++20)std::bit_cast, not aunion. - A
unionwith a non-trivial member needs hand-written lifetime management. -
std::get<T>/std::get<I>throw on the wrong type;std::get_ifreturnsnullptrinstead — preferget_ifin hot paths. -
std::optional<T>is not a pointer —*optdereferences the value, but there's no null; check withif (opt)first.