Kotlin to C/C++ Transition Guide: A Systems Programmer's Cheat Sheet
Preparing for a systems programming interview but haven't touched C/C++ since university? This guide bridges your Kotlin knowledge to C/C++ with side-by-side syntax comparisons, memory management deep dives, and critical undefined behaviors you need to know.
Table of contents
- Kotlin to C/C++ Transition Cheat Sheet for ARM Systems Programming
- 1. Syntax and basic concepts comparison
- Variable declarations and type inference
- Functions
- Control flow
- Null handling differences are critical
- 2. Memory management
- Pointers vs references
- C++ references: lvalue and rvalue
- Stack vs heap allocation
- malloc/free vs new/delete
- RAII pattern: resource acquisition is initialization
- Smart pointers replace garbage collection
- Common memory errors to avoid
- 3. Object-oriented basics
- Structs vs classes
- Classes compared
- Virtual functions and vtables
- Multiple inheritance and the diamond problem
- Abstract classes and interfaces
- 4. Concurrency
- C concurrency with pthreads
- Mutexes and condition variables
- C++ concurrency is cleaner with RAII
- Atomic operations
- Kotlin coroutines vs C++ threads: fundamental differences
- ARM-specific concurrency considerations
- 5. Compile-time behavior
- C preprocessor
- C++ templates vs Kotlin generics
- constexpr vs const
- Template metaprogramming basics
- 6. Error handling
- C-style error handling
- C++ exceptions
- RAII provides exception safety
- noexcept is critical for performance
- When NOT to use exceptions on ARM
- Comparison with Kotlin
- Quick reference for interview preparation
Kotlin to C/C++ Transition Cheat Sheet for ARM Systems Programming
Transitioning from Kotlin to C/C++ requires a fundamental mental shift: you now manage memory explicitly, synchronization is preemptive rather than cooperative, and compilation behavior differs radically. This cheat sheet provides side-by-side comparisons with ARM embedded context, targeting the key concepts you’ll encounter in systems programming interviews. Each section covers concept definitions, comparative syntax across C, C++ (C++11/14/17), and Kotlin, memory and compilation differences, and critical gotchas.
1. Syntax and basic concepts comparison
Variable declarations and type inference
C requires explicit type declarations, while C++11 introduced auto for compile-time type inference—similar to Kotlin’s val/var but with zero runtime overhead.
// C - Explicit types required
int x = 42;
const int y = 100; // Read-only
uint32_t counter = 0; // Use stdint.h for exact sizes on ARM
// C++ (C++11/14/17) - auto keyword for type inference
auto x = 42; // int inferred at compile-time
auto& ref = x; // int& reference
const auto z = "Hello"; // const char* inferred
// C++17: Structured bindings
auto [a, b] = std::make_pair(1, 2);
// Kotlin - val (immutable reference) and var (mutable reference)
val x = 42 // Int inferred, read-only reference
var y = 3.14 // Double inferred, mutable
const val PI = 3.14159 // True compile-time constant
Under the hood (ARM): C++ auto resolves entirely at compile-time with zero overhead. For embedded ARM, prefer explicit fixed-width types (uint32_t, int16_t from <stdint.h>) for predictable memory layout. Kotlin’s val is read-only but not truly immutable—the underlying object can mutate.
Gotchas:
- C++
autodropsconstand references unless explicitly specified (auto&,const auto) - C++
auto x;without initialization is invalid—unlike Kotlin’slateinit - Kotlin
valprevents reassignment but doesn’t guarantee immutability of the object itself
Functions
// C - No overloading, no defaults
int add(int a, int b) { return a + b; }
int (*operation)(int, int) = add; // Function pointer
// C++ - Overloading, defaults, trailing return types
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; } // Overload
int multiply(int a, int b = 1) { return a * b; } // Default param
auto divide(int a, int b) -> double { // Trailing return
return static_cast<double>(a) / b;
}
auto lambda = [](int x) { return x * 2; }; // Lambda (C++11)
constexpr int square(int x) { return x * x; } // Compile-time
// Kotlin - Concise expression syntax
fun add(a: Int, b: Int): Int = a + b
fun greet(name: String, greeting: String = "Hello") = "$greeting, $name"
val double = { x: Int -> x * 2 } // Lambda
fun String.addBang() = this + "!" // Extension function
Under the hood (ARM): constexpr functions are computed at compile-time—ideal for ARM where Flash is plentiful but RAM is scarce. Kotlin extension functions compile to static methods with the receiver as the first parameter.
Control flow
// C - if/else, switch (integers/enums only)
switch (x) {
case 1: printf("One"); break; // break required!
case 2:
case 3: printf("Two/Three"); break;
default: printf("Other");
}
// C++ (C++17) - if/switch with initializer
if (auto result = compute(); result > 0) { /* use result */ }
// Range-based for (C++11)
std::vector<int> vec = {1, 2, 3};
for (const auto& item : vec) { std::cout << item; }
// Kotlin - when expression (switch replacement, is an expression)
val result = when (x) {
1 -> "One"
2, 3 -> "Two or Three"
in 4..10 -> "Between 4-10"
is String -> "It's a string"
else -> "Other"
}
Under the hood (ARM): C/C++ switch statements compile to efficient jump tables on ARM. Kotlin’s when on the JVM uses tableswitch/lookupswitch bytecode with similar efficiency for dense integer ranges.
Null handling differences are critical
This represents the most significant safety difference between the languages. Kotlin enforces null safety at compile-time; C/C++ does not.
// C - NULL is macro for 0 (type ambiguity issues)
int* ptr = NULL;
if (ptr != NULL) { *ptr = 10; } // Manual check required
// Danger: NULL is integer 0, causes overload ambiguity
// C++ - nullptr is type-safe (C++11)
int* ptr = nullptr;
process(nullptr); // Calls process(int*), not process(int)
// std::optional (C++17) for nullable values
std::optional<int> maybeValue = std::nullopt;
int v = maybeValue.value_or(0); // Default if empty
// Kotlin - Compile-time null safety
var name: String = "Hello" // Cannot be null
var nullable: String? = null // Nullable type
val length = nullable?.length // Safe call, returns null
val len = nullable?.length ?: 0 // Elvis operator default
val unsafe = nullable!!.length // Throws if null (avoid!)
| Feature | C | C++ | Kotlin |
|---|---|---|---|
| Null representation | NULL (= 0) | nullptr | null |
| Type safety | None | Yes | Yes + compile-time |
| Optional values | Not native | std::optional | ? suffix |
Under the hood (ARM): In bare-metal ARM, null pointer dereference may not crash—it could silently corrupt memory at address 0. Use static analysis tools (PC-lint, Polyspace) to catch null issues. Kotlin’s null safety has JVM overhead and isn’t available in bare-metal contexts.
Gotchas:
- Never use
NULLin modern C++—alwaysnullptr - Dereferencing
nullptris undefined behavior (not guaranteed to crash) - Kotlin
!!defeats null safety—use sparingly - Platform types from Java interop bypass Kotlin’s null checks
2. Memory management
Pointers vs references
C/C++ provide direct memory manipulation through pointers. Kotlin uses garbage-collected references with no direct memory access.
// C - Raw pointers and pointer arithmetic
int x = 42;
int* ptr = &x; // Address-of operator
*ptr = 100; // Dereference and modify
int arr[5] = {1,2,3,4,5};
int* p = arr;
p++; // Points to arr[1] - pointer arithmetic
// C++ - Pointers plus references
int x = 42;
int* ptr = &x; // Pointer
int& ref = x; // Lvalue reference (alias to x)
ref = 100; // Modifies x directly, no dereference needed
// const correctness (critical for APIs)
const int* ptr1 = &x; // Pointer to const int (data protected)
int* const ptr2 = &x; // Const pointer to int (pointer protected)
// Kotlin - References only, no pointers
val obj = Data(42)
val ref2 = obj // Reference to same object
ref2.value = 100 // obj.value is now 100
// No pointer arithmetic, no & operator
Under the hood (ARM): Direct pointer manipulation is essential for memory-mapped I/O on ARM:
volatile uint32_t* GPIO_PORT = (volatile uint32_t*)0x40020000;
*GPIO_PORT |= (1 << 5); // Set pin 5 high
Kotlin/JVM cannot directly access hardware. Kotlin/Native has limited pointer support via CPointer.
C++ references: lvalue and rvalue
Understanding lvalue/rvalue references is critical for move semantics and efficient ARM code.
// Lvalue reference (&) - alias to existing object
int x = 10;
int& lref = x; // OK: x is lvalue
// int& lref2 = 10; // ERROR: can't bind to rvalue
// Rvalue reference (&&) - binds to temporaries (C++11)
int&& rref = 10; // OK: binds to temporary
// Move semantics - transfers ownership, avoids copies
class Buffer {
int* data; size_t size;
public:
Buffer(Buffer&& other) noexcept // Move constructor
: data(other.data), size(other.size) {
other.data = nullptr; // Leave source valid but empty
}
};
Buffer b2 = std::move(b1); // Moves resources, b1 now empty
Under the hood (ARM): Move semantics avoid expensive deep copies—critical for large buffers on memory-limited ARM Cortex-M. Mark move operations noexcept to enable STL optimizations.
Stack vs heap allocation
| Environment | Stack Size | Heap | Recommendation |
|---|---|---|---|
| Bare-metal ARM | 1-8 KB | Often disabled | Stack only, static allocation |
| RTOS (FreeRTOS) | 512B-4KB per task | Limited pool | Static + memory pools |
| Linux embedded | 8 MB default | Full | Use carefully, avoid fragmentation |
// C - Stack allocation (automatic cleanup)
void func() {
int local = 42; // Stack
int arr[100]; // Stack array
} // Automatically deallocated
// C - Heap allocation
int* heap_arr = (int*)malloc(100 * sizeof(int));
if (heap_arr == NULL) { /* handle failure */ }
free(heap_arr);
heap_arr = NULL; // Prevent dangling pointer
// C++ - Prefer smart pointers
auto unique = std::make_unique<int>(42); // Heap, auto-deleted
auto shared = std::make_shared<Buffer>(); // Reference counted
// Bare-metal pattern: static allocation with placement new
alignas(T) uint8_t pool[sizeof(T) * N];
T* obj = new (&pool[0]) T(); // Placement new in pre-allocated memory
malloc/free vs new/delete
| Feature | malloc/free | new/delete |
|---|---|---|
| Constructor/Destructor | Not called | Called automatically |
| Type safety | Returns void* | Returns typed pointer |
| Failure behavior | Returns NULL | Throws std::bad_alloc |
| Size | Manual sizeof | Automatic |
// NEVER mix: malloc with delete, or new with free!
Point* p = new Point(10, 20); // Constructor called
delete p; // Destructor called
// Embedded: disable exceptions
Point* p = new (std::nothrow) Point(10, 20);
if (p == nullptr) { /* handle failure */ }
RAII pattern: resource acquisition is initialization
RAII ties resource lifetime to object lifetime—the most important C++ idiom for Kotlin developers to learn.
void raii_example() {
std::lock_guard<std::mutex> lock(mtx); // Locked here
auto data = std::make_unique<int[]>(1000); // Allocated here
std::ifstream file("data.txt"); // Opened here
if (error) return; // All resources still released!
throw std::runtime_error("oops"); // Still released!
} // Destructor releases mutex, memory, file handle automatically
Kotlin comparison: Kotlin uses finally blocks and use extension for cleanup. C++ uses destructors—more reliable because they’re automatic and can’t be forgotten.
Smart pointers replace garbage collection
// unique_ptr: Exclusive ownership (zero overhead vs raw pointer)
auto ptr = std::make_unique<int>(42);
// auto copy = ptr; // ERROR: cannot copy
auto moved = std::move(ptr); // OK: ptr now nullptr
// shared_ptr: Shared ownership with reference counting
auto shared = std::make_shared<int>(42);
auto copy = shared; // ref_count = 2
// Last shared_ptr destructor deletes object
// weak_ptr: Non-owning observer (breaks reference cycles)
std::weak_ptr<Node> parent; // Doesn't prevent deletion
if (auto locked = parent.lock()) { /* use safely */ }
ARM considerations: shared_ptr has overhead (control block + atomic ref counting). unique_ptr has zero overhead compared to raw pointers. For bare-metal, consider lightweight custom smart pointers.
Common memory errors to avoid
// 1. Dangling pointer (use-after-free)
int* ptr = new int(42);
delete ptr;
*ptr = 100; // UNDEFINED BEHAVIOR!
// Fix: ptr = nullptr after delete; use smart pointers
// 2. Double free
free(ptr);
free(ptr); // CRASH or heap corruption
// Fix: ptr = NULL after free (free(NULL) is safe no-op)
// 3. Buffer overflow
char buf[10];
strcpy(buf, "This is too long!"); // Stack smashing!
// Fix: strncpy(buf, src, sizeof(buf)-1); buf[sizeof(buf)-1] = '\0';
// 4. Memory leak
void leak() {
auto data = new int[1000];
if (error) return; // Never deleted!
}
// Fix: Use RAII/smart pointers
3. Object-oriented basics
Structs vs classes
// C struct: data only, no methods, no access control
struct Point { int x; int y; };
struct Point p1; // Requires 'struct' keyword (or typedef)
// C++ struct: nearly identical to class (default public)
struct Point {
int x = 0, y = 0; // In-class initialization
Point(int x, int y) : x(x), y(y) {} // Constructor
void print() const { std::cout << x << "," << y; }
};
// Convention: struct for POD/passive data, class for behavior
ARM note: C structs and C++ structs without virtual functions have identical memory layout with zero overhead.
Classes compared
// C++ class (default private access)
class Vehicle {
private:
std::string name_;
public:
explicit Vehicle(std::string name) : name_(std::move(name)) {}
virtual ~Vehicle() = default; // Virtual if inheritance planned
virtual void move() { std::cout << name_ << " moving\n"; }
};
// Kotlin class (final by default, must use 'open' for inheritance)
open class Vehicle(private val name: String) {
open fun move() { println("$name moving") }
}
| Feature | C++ | Kotlin |
|---|---|---|
| Default inheritance | Open | Final (open keyword needed) |
| Default member access | Private | Public |
| Virtual by default | No (virtual needed) | No (open needed) |
| Properties | Manual getters/setters | Built-in |
Virtual functions and vtables
When you mark a function virtual, the compiler creates a vtable (array of function pointers) for each class.
class Animal {
public:
virtual void speak() { std::cout << "Animal\n"; }
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Woof!\n"; }
};
Animal* ptr = new Dog();
ptr->speak(); // Output: "Woof!" (runtime dispatch via vtable)
Memory layout:
Dog Object: Dog VTable:
+----------+ +----------------+
| vptr ----+-----------> | &Dog::speak |
+----------+ | &Animal::~dtor |
| data | +----------------+
+----------+
ARM overhead:
- +4 bytes per object (vptr on 32-bit ARM)
- One vtable per class in .rodata (Flash)
- Two memory accesses per virtual call + indirect branch
- Cannot be inlined (resolved at runtime)
Optimization: Use final keyword when a class won’t be derived—allows devirtualization.
Multiple inheritance and the diamond problem
// Diamond problem: D inherits A twice through B and C
class A { public: int x; };
class B : public A { };
class C : public A { };
class D : public B, public C { }; // D has TWO copies of A::x!
// Solution: Virtual inheritance (single shared base)
class B : virtual public A { };
class C : virtual public A { };
class D : public B, public C { }; // D has ONE copy of A::x
Kotlin comparison: Kotlin uses interfaces with default methods instead of multiple inheritance. Conflicts require explicit resolution with super<Interface>.method().
Abstract classes and interfaces
// C++ abstract class (pure virtual = 0)
class Shape {
public:
virtual double area() const = 0; // Pure virtual
void printInfo() const { std::cout << area(); } // Concrete OK
virtual ~Shape() = default;
};
// Kotlin abstract class
abstract class Shape {
abstract fun area(): Double
fun printInfo() { println(area()) } // Concrete OK
}
// Kotlin interface (can have default implementations, no state)
interface Drawable {
fun draw()
fun resize(w: Int, h: Int) { println("Resizing to $w x $h") }
}
| Feature | C++ Pure Virtual Class | Kotlin Interface |
|---|---|---|
| Default implementations | By convention, no | Yes |
| State (fields) | Can have, but shouldn’t | Cannot |
| Constructor | Can have | Cannot |
| vtable overhead | Yes | Similar (JVM) |
4. Concurrency
C concurrency with pthreads
#include <pthread.h>
void* worker(void* arg) {
int* val = (int*)arg;
printf("Thread got: %d\n", *val);
pthread_exit(NULL);
}
int main() {
pthread_t thread;
int data = 42;
pthread_create(&thread, NULL, worker, &data);
pthread_join(thread, NULL); // Wait for completion
return 0;
}
// Compile: gcc -pthread program.c
Mutexes and condition variables
// Mutex for mutual exclusion
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&lock);
// Critical section
pthread_mutex_unlock(&lock);
// Condition variable - ALWAYS use while loop (spurious wakeups!)
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_lock(&mutex);
while (!condition) { // NOT if!
pthread_cond_wait(&cond, &mutex); // Atomically unlocks, waits, relocks
}
pthread_mutex_unlock(&mutex);
C++ concurrency is cleaner with RAII
#include <thread>
#include <mutex>
std::mutex mtx;
int counter = 0;
void safe_increment() {
std::lock_guard<std::mutex> guard(mtx); // RAII lock
counter++;
} // Auto-unlocked on any exit path
// C++17: scoped_lock for multiple mutexes (deadlock-free)
std::scoped_lock lock(mtx1, mtx2);
// std::async for fire-and-forget tasks
auto future = std::async(std::launch::async, compute, arg);
int result = future.get(); // Blocks until done
Atomic operations
#include <atomic>
std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed); // No mutex needed
// Memory ordering (from weakest to strongest)
// memory_order_relaxed - only atomicity, no ordering
// memory_order_acquire - prevents reordering loads before
// memory_order_release - prevents reordering stores after
// memory_order_seq_cst - total global ordering (default, safest)
Kotlin coroutines vs C++ threads: fundamental differences
| Aspect | C/C++ Threads | Kotlin Coroutines |
|---|---|---|
| Scheduling | Preemptive (OS kernel) | Cooperative (user-space) |
| Context switch | ~1-10μs, kernel mode | ~100ns, user mode |
| Memory per unit | ~2MB stack | ~few KB |
| Blocking | Blocks OS thread | Suspends coroutine only |
| Mental model | Can be interrupted anywhere | Only at suspend points |
// Kotlin: Structured concurrency (automatic cleanup)
suspend fun fetchData() = coroutineScope {
val profile = async { fetchProfile() }
val orders = async { fetchOrders() }
UserData(profile.await(), orders.await())
} // If any fails, siblings cancelled; scope ensures cleanup
// Kotlin Mutex is SUSPENDING (doesn't block thread!)
val mutex = Mutex()
mutex.withLock { counter++ } // Suspends if locked, doesn't block
Key mental shifts for Kotlin developers:
- Threads can be interrupted anywhere—not just at suspension points
- No structured concurrency—manual lifecycle management required
- C++
std::mutex::lock()blocks the thread; Kotlin’s suspends the coroutine - Race conditions can occur between any two instructions
ARM-specific concurrency considerations
Memory barriers on ARM: ARM uses a weakly-ordered memory model. Explicit barriers often required:
// DMB - Data Memory Barrier (ordering)
// DSB - Data Synchronization Barrier (completion)
// ISB - Instruction Synchronization Barrier (pipeline)
__asm__ volatile("dmb sy" ::: "memory");
volatile vs atomics:
| Use Case | volatile | atomic |
|---|---|---|
| Hardware registers | ✅ | ❌ |
| ISR → main (single word) | ✅ | ✅ |
| Read-modify-write | ❌ | ✅ |
| Multi-threaded | ❌ | ✅ |
// volatile: prevents compiler caching, NOT atomic, NO memory barriers
volatile uint32_t* UART = (volatile uint32_t*)0x40001000;
// atomic: guarantees atomicity AND optional memory ordering
_Atomic uint32_t flag;
atomic_store(&flag, 1); // With memory barrier
5. Compile-time behavior
C preprocessor
// Object-like macro
#define BUFFER_SIZE 1024
#define GPIO_BASE 0x40020000
// Function-like macro (ALWAYS parenthesize!)
#define MAX(A, B) ((A) > (B) ? (A) : (B))
#define REG_WRITE(addr, val) (*(volatile uint32_t*)(addr) = (val))
// Conditional compilation
#ifdef DEBUG
#define LOG(msg) printf("[DEBUG] %s\n", msg)
#else
#define LOG(msg) ((void)0) // Compiles to nothing
#endif
// Header guard (prevents multiple inclusion)
#ifndef MYHEADER_H
#define MYHEADER_H
// ... contents ...
#endif
Macro pitfalls:
// BAD: Missing parentheses
#define SQUARE(x) x * x
int k = SQUARE(2 + 1); // Expands to 2 + 1 * 2 + 1 = 5, not 9!
// BAD: Double evaluation
int result = MAX(i++, j++); // i or j incremented TWICE!
// GOOD: Use inline functions for complex cases
static inline int max_int(int a, int b) { return a > b ? a : b; }
C++ templates vs Kotlin generics
Fundamental difference: C++ templates are resolved at compile-time (monomorphization), generating separate code for each type. Kotlin generics use type erasure at runtime.
// C++ function template
template <typename T>
T add(T a, T b) { return a + b; }
add(5, 3); // Compiler generates add<int>
add(2.5, 1.5); // Compiler generates add<double>
// C++ class template for embedded
template <typename T, size_t Size>
class CircularBuffer {
T buffer[Size]; // Statically allocated - no heap!
// ...
};
CircularBuffer<uint8_t, 64> uart_buffer;
// Kotlin: Type erasure (types not available at runtime)
val strings: List<String> = listOf("a", "b")
val ints: List<Int> = listOf(1, 2)
// At runtime, both are just List (erased)
// reified: preserves type info (inline functions only)
inline fun <reified T> isInstance(value: Any) = value is T
| Aspect | C++ Templates | Kotlin Generics |
|---|---|---|
| Resolution | Compile-time | Runtime (erased) |
| Code generation | Separate code per type | Single implementation |
| Type info at runtime | No | No (unless reified) |
| Binary size impact | Can bloat | Minimal |
| Error messages | Complex (better in C++20) | Clearer |
constexpr vs const
// C: const is "read-only variable" - still runtime storage
const int size = 10;
int arr[size]; // ERROR in C89! VLA in C99
// C++: const with constant initializer IS compile-time
const int size = 10;
int arr[size]; // OK in C++
// constexpr: GUARANTEED compile-time evaluation
constexpr int square(int x) { return x * x; }
constexpr int val = square(5); // Computed at compile-time
// consteval (C++20): MUST be compile-time
consteval int must_compile_time(int x) { return x * x; }
ARM benefit: constexpr data goes to Flash/ROM, not precious RAM.
Template metaprogramming basics
// if constexpr (C++17) - compile-time conditional
template <typename T>
auto process(T value) {
if constexpr (std::is_integral_v<T>) {
return value * 2;
} else if constexpr (std::is_floating_point_v<T>) {
return value * 2.5;
}
}
// Type traits for compile-time checks
static_assert(std::is_trivially_copyable_v<MyStruct>,
"DMA requires trivially copyable types");
ARM embedded flags:
arm-none-eabi-g++ -fno-rtti -fno-exceptions -Os \
-ffunction-sections -fdata-sections -Wl,--gc-sections
6. Error handling
C-style error handling
typedef enum { SUCCESS = 0, ERR_NULL = -1, ERR_INVALID = -2 } ErrorCode;
ErrorCode divide(int a, int b, int* result) {
if (result == NULL) return ERR_NULL;
if (b == 0) return ERR_INVALID;
*result = a / b;
return SUCCESS;
}
// errno for system calls
FILE* f = fopen("data.txt", "r");
if (f == NULL) {
perror("fopen"); // Prints "fopen: No such file or directory"
}
Pros: Zero overhead on success, deterministic timing, works on bare-metal. Cons: Easy to ignore, clutters code, error propagation is tedious.
C++ exceptions
class FileError : public std::runtime_error {
public:
explicit FileError(const std::string& msg) : std::runtime_error(msg) {}
};
void process() {
try {
File f("data.txt"); // Throws on failure
} catch (const FileError& e) {
std::cerr << "File error: " << e.what() << "\n";
} catch (...) {
std::cerr << "Unknown error\n";
}
}
RAII provides exception safety
void safe_function() {
auto data = std::make_unique<int[]>(1000);
std::lock_guard<std::mutex> lock(mtx);
may_throw(); // Even if this throws...
} // ...memory freed and mutex released automatically
Exception safety levels:
- No-throw: Function never throws (
noexcept) - Strong: Operation succeeds completely or state is unchanged
- Basic: No leaks, invariants preserved, but state may change
- None: Avoid this!
noexcept is critical for performance
// noexcept enables optimizations
class Widget {
public:
Widget(Widget&& other) noexcept; // Move must be noexcept
~Widget() noexcept; // Destructors implicitly noexcept
};
// Why it matters:
std::vector<Widget> widgets;
widgets.reserve(100);
// If move ctor is noexcept: uses move (fast)
// If NOT noexcept: falls back to copy (slow)
When NOT to use exceptions on ARM
Problems with exceptions on embedded:
- Code size: Exception tables add 10-30% to binary
- Non-deterministic timing: Unsuitable for hard real-time
- Memory allocation: Some implementations allocate on throw
- Library support:
newlib-nanodoesn’t support exceptions by default
Disable with: arm-none-eabi-g++ -fno-exceptions -fno-rtti
Use instead:
// Return structured results
struct Result { enum Status { Ok, Error } status; int value; };
// std::optional for nullable returns
std::optional<int> tryParse(const char* str) noexcept;
// std::expected (C++23) for error info
std::expected<int, ErrorCode> divide(int a, int b) noexcept;
Comparison with Kotlin
// Kotlin: All exceptions unchecked, try is an expression
val result = try {
parseInt(input)
} catch (e: NumberFormatException) {
-1
}
// Result type for functional error handling
fun divide(a: Int, b: Int): Result<Int> =
if (b == 0) Result.failure(ArithmeticException())
else Result.success(a / b)
divide(10, 2)
.map { it * 2 }
.getOrElse { 0 }
| Feature | C++ | Kotlin |
|---|---|---|
| Checked exceptions | No | No |
| try as expression | No | Yes |
| finally | No (use RAII) | Yes |
| Functional (Result) | std::optional/expected | Built-in Result<T> |
Quick reference for interview preparation
Memory model differences:
- Kotlin: GC managed, no manual control, non-deterministic cleanup
- C++: RAII + smart pointers, deterministic cleanup, full control
- C: Manual malloc/free, your responsibility entirely
Concurrency differences:
- Kotlin: Cooperative coroutines, suspend points, structured concurrency
- C++: Preemptive threads, can interrupt anywhere, manual lifecycle
- ARM: Weakly-ordered memory, explicit barriers required
Type system differences:
- Kotlin: Null safety built-in, type erasure at runtime
- C++: No null safety, templates = compile-time code generation
- C: No generics, macros for “generic” code
Key C++ idioms to master:
- RAII - Resources tied to object lifetime
- Smart pointers -
unique_ptr(default),shared_ptr(when sharing) - const correctness - Use
consteverywhere possible - Move semantics -
std::movefor transfer,noexceptfor efficiency auto+ range-for - Modern, readable iteration
ARM embedded best practices:
- Use fixed-width types (
uint32_t,int16_t) - Prefer stack/static allocation over heap
- Use
constexprfor compile-time computation (data in Flash) - Mark functions
noexceptfor predictable behavior - Use
volatilefor hardware registers,atomicfor thread safety - Disable exceptions/RTTI for minimal binary size
Share
More to explore
Keep exploring
1/10/2026
The SDK Mindset: Why Your Code Isn't Your Own Anymore
A deep dive into the paradigm shift from application development to SDK design, and why building libraries requires a fundamentally different mental model
12/16/2025
Building a Frictionless Android Sample App: README Funnel, Doctor Script, and a CI Compatibility Matrix
My AI-assisted learning log on turning an Android SDK demo into a low-friction client experience: a decision-tree README, environment doctor scripts, and a GitHub Actions build matrix that generates a compatibility matrix.
12/15/2025
Android Learning Log: Compose Permissions, SavedStateHandle, and Testing Pitfalls
A developer-friendly learning log on fixing a Jetpack Compose permission flow, understanding SavedStateHandle/nav args, and debugging MockK + Robolectric Compose tests—complete with real-world analogies and code snippets.
Next