21 min read

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.

#c++#kotlin#systems-programming#interview-prep#embedded

Table of contents

  1. Kotlin to C/C++ Transition Cheat Sheet for ARM Systems Programming
  2. 1. Syntax and basic concepts comparison
  3. Variable declarations and type inference
  4. Functions
  5. Control flow
  6. Null handling differences are critical
  7. 2. Memory management
  8. Pointers vs references
  9. C++ references: lvalue and rvalue
  10. Stack vs heap allocation
  11. malloc/free vs new/delete
  12. RAII pattern: resource acquisition is initialization
  13. Smart pointers replace garbage collection
  14. Common memory errors to avoid
  15. 3. Object-oriented basics
  16. Structs vs classes
  17. Classes compared
  18. Virtual functions and vtables
  19. Multiple inheritance and the diamond problem
  20. Abstract classes and interfaces
  21. 4. Concurrency
  22. C concurrency with pthreads
  23. Mutexes and condition variables
  24. C++ concurrency is cleaner with RAII
  25. Atomic operations
  26. Kotlin coroutines vs C++ threads: fundamental differences
  27. ARM-specific concurrency considerations
  28. 5. Compile-time behavior
  29. C preprocessor
  30. C++ templates vs Kotlin generics
  31. constexpr vs const
  32. Template metaprogramming basics
  33. 6. Error handling
  34. C-style error handling
  35. C++ exceptions
  36. RAII provides exception safety
  37. noexcept is critical for performance
  38. When NOT to use exceptions on ARM
  39. Comparison with Kotlin
  40. 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++ auto drops const and references unless explicitly specified (auto&, const auto)
  • C++ auto x; without initialization is invalid—unlike Kotlin’s lateinit
  • Kotlin val prevents 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!)
FeatureCC++Kotlin
Null representationNULL (= 0)nullptrnull
Type safetyNoneYesYes + compile-time
Optional valuesNot nativestd::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 NULL in modern C++—always nullptr
  • Dereferencing nullptr is 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

EnvironmentStack SizeHeapRecommendation
Bare-metal ARM1-8 KBOften disabledStack only, static allocation
RTOS (FreeRTOS)512B-4KB per taskLimited poolStatic + memory pools
Linux embedded8 MB defaultFullUse 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

Featuremalloc/freenew/delete
Constructor/DestructorNot calledCalled automatically
Type safetyReturns void*Returns typed pointer
Failure behaviorReturns NULLThrows std::bad_alloc
SizeManual sizeofAutomatic
// 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") }
}
FeatureC++Kotlin
Default inheritanceOpenFinal (open keyword needed)
Default member accessPrivatePublic
Virtual by defaultNo (virtual needed)No (open needed)
PropertiesManual getters/settersBuilt-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") }
}
FeatureC++ Pure Virtual ClassKotlin Interface
Default implementationsBy convention, noYes
State (fields)Can have, but shouldn’tCannot
ConstructorCan haveCannot
vtable overheadYesSimilar (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

AspectC/C++ ThreadsKotlin Coroutines
SchedulingPreemptive (OS kernel)Cooperative (user-space)
Context switch~1-10μs, kernel mode~100ns, user mode
Memory per unit~2MB stack~few KB
BlockingBlocks OS threadSuspends coroutine only
Mental modelCan be interrupted anywhereOnly 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:

  1. Threads can be interrupted anywhere—not just at suspension points
  2. No structured concurrency—manual lifecycle management required
  3. C++ std::mutex::lock() blocks the thread; Kotlin’s suspends the coroutine
  4. 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 Casevolatileatomic
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
AspectC++ TemplatesKotlin Generics
ResolutionCompile-timeRuntime (erased)
Code generationSeparate code per typeSingle implementation
Type info at runtimeNoNo (unless reified)
Binary size impactCan bloatMinimal
Error messagesComplex (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:

  1. No-throw: Function never throws (noexcept)
  2. Strong: Operation succeeds completely or state is unchanged
  3. Basic: No leaks, invariants preserved, but state may change
  4. 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-nano doesn’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 }
FeatureC++Kotlin
Checked exceptionsNoNo
try as expressionNoYes
finallyNo (use RAII)Yes
Functional (Result)std::optional/expectedBuilt-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:

  1. RAII - Resources tied to object lifetime
  2. Smart pointers - unique_ptr (default), shared_ptr (when sharing)
  3. const correctness - Use const everywhere possible
  4. Move semantics - std::move for transfer, noexcept for efficiency
  5. 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 constexpr for compile-time computation (data in Flash)
  • Mark functions noexcept for predictable behavior
  • Use volatile for hardware registers, atomic for thread safety
  • Disable exceptions/RTTI for minimal binary size

Share

More to explore

Keep exploring

Next

The SDK Mindset: Why Your Code Isn't Your Own Anymore