Skip to content
C++
Domain Deep-Dive
Expert

Freestanding C++ Without the Standard Library

"Writing C++ without libc or libstdc++: freestanding headers, replacing stdlib facilities, placement new, and what you can and cannot use on bare metal."

TL;DR

Freestanding C++ is the subset of the language that works without an operating system or standard library implementation. You lose <iostream>, <string>, most containers, and exceptions — but keep templates, lambdas, constexpr, RAII, and a small set of freestanding headers. Use -ffreestanding to opt in; provide your own operator new if you need dynamic allocation.

Freestanding vs hosted

cpp
Hosted (normal C++):
  - Full standard library available
  - OS provides: heap, threading, file I/O, exceptions
  - Program starts at main(), runtime set up by cstartup

Freestanding:
  - Minimal standard library
  - No OS — you provide everything
  - main() is optional; entry point is your Reset_Handler
  - Compile with: -ffreestanding -nostdlib (or -nodefaultlibs)

What's available in freestanding C++

Per C++20 [intro.compliance]:

HeaderAvailable
<cstddef>size_t, ptrdiff_t, nullptr_t, byte, offsetof
<cstdint>int32_t, uint64_t, etc.
<cfloat>float limits
<climits>integer limits
<cstdlib>abort(), atexit() (but not malloc)
<new>placement new, bad_alloc type
<type_traits>all type traits
<concepts>all concepts (C++20)
<utility>move, forward, swap, pair
<tuple>tuple (C++23)
<array>std::array (no heap)
<atomic>atomics (C++20, hardware support req'd)
<bit>bit_cast, popcount, etc. (C++20)
<compare>three-way comparison

Not available: <string>, <vector>, <map>, <iostream>, <algorithm> (partial), <thread>, <mutex>, <exception>.

Compiler flags

cmake
# CMakeLists.txt for freestanding target
target_compile_options(firmware PRIVATE
    -ffreestanding      # no stdlib assumptions
    -fno-exceptions     # no exception machinery
    -fno-rtti           # no runtime type info
    -fno-threadsafe-statics  # no thread-safe init for local statics
    -nostdlib           # don't link any standard libraries
    -nodefaultlibs      # don't add default library search paths
)

# If you need the compiler runtime (soft-float, integer division)
# link libgcc manually:
target_link_libraries(firmware PRIVATE gcc)

Providing your own operator new

Without libstdc++, new is undefined. Implement it yourself:

cpp
// memory.cpp — compiled into your firmware

#include <new>
#include <cstddef>

// Simple bump allocator (never frees — appropriate for static init)
namespace {
    constexpr size_t HEAP_SIZE = 64 * 1024;
    alignas(alignof(std::max_align_t)) uint8_t heap[HEAP_SIZE];
    size_t heap_ptr = 0;
}

void* operator new(size_t size) noexcept {
    size = (size + alignof(std::max_align_t) - 1)
           & ~(alignof(std::max_align_t) - 1);
    if (heap_ptr + size > HEAP_SIZE) return nullptr;  // OOM
    void* p = &heap[heap_ptr];
    heap_ptr += size;
    return p;
}

void* operator new[](size_t size) noexcept { return operator new(size); }

// delete is no-op — bump allocator doesn't free
void operator delete(void*) noexcept {}
void operator delete[](void*) noexcept {}
void operator delete(void*, size_t) noexcept {}
void operator delete[](void*, size_t) noexcept {}

Placement new — stack-constructed objects

cpp
#include <new>

// Construct an object in pre-allocated storage — no heap required
alignas(MyClass) uint8_t storage[sizeof(MyClass)];
MyClass* obj = new (storage) MyClass(arg1, arg2);  // placement new

// Must call destructor manually (no delete)
obj->~MyClass();

Implementing missing facilities

memcpy / memset (required by the compiler)

cpp
// The compiler may emit calls to these even with -ffreestanding
// You must provide them or link against a freestanding libc

extern "C" {

void* memcpy(void* __restrict__ dst, const void* __restrict__ src, size_t n) {
    auto* d = static_cast<uint8_t*>(dst);
    auto* s = static_cast<const uint8_t*>(src);
    while (n--) *d++ = *s++;
    return dst;
}

void* memset(void* dst, int c, size_t n) {
    auto* d = static_cast<uint8_t*>(dst);
    while (n--) *d++ = static_cast<uint8_t>(c);
    return dst;
}

void* memmove(void* dst, const void* src, size_t n) {
    auto* d = static_cast<uint8_t*>(dst);
    auto* s = static_cast<const uint8_t*>(src);
    if (d < s) { while (n--) *d++ = *s++; }
    else        { d += n; s += n; while (n--) *--d = *--s; }
    return dst;
}

int memcmp(const void* a, const void* b, size_t n) {
    auto* p = static_cast<const uint8_t*>(a);
    auto* q = static_cast<const uint8_t*>(b);
    while (n--) {
        if (*p != *q) return *p - *q;
        ++p; ++q;
    }
    return 0;
}

} // extern "C"

__cxa_pure_virtual — required when you use virtual functions

cpp
// Replaces the default libstdc++ handler which calls terminate()
extern "C" void __cxa_pure_virtual() {
    // In debug builds: break to debugger
    __asm("bkpt #0");
    while (true) {}
}

std::abort

cpp
// If the compiler generates abort() calls (assert, etc.)
extern "C" void abort() {
    __asm("bkpt #0");
    while (true) {}
}

Using type_traits in freestanding

Type traits work fully in freestanding — they're pure template metaprogramming:

cpp
#include <type_traits>
#include <cstdint>

// Statically-sized register map with type-checked access
template<typename T>
    requires std::is_trivially_copyable_v<T>
void writeRegister(volatile uint32_t* reg, T value) {
    static_assert(sizeof(T) <= 4, "register write too wide");
    *reg = static_cast<uint32_t>(value);
}

// SFINAE-based serialization for fixed-width types
template<typename T>
    requires std::is_integral_v<T>
void serialize(uint8_t* buf, T val) {
    for (size_t i = 0; i < sizeof(T); ++i)
        buf[i] = static_cast<uint8_t>(val >> (i * 8));
}

Fixed-size string for freestanding

cpp
template<size_t N>
class FixedString {
public:
    FixedString() { buf_[0] = '\0'; len_ = 0; }

    explicit FixedString(const char* s) {
        len_ = 0;
        while (s[len_] && len_ < N - 1) {
            buf_[len_] = s[len_];
            ++len_;
        }
        buf_[len_] = '\0';
    }

    void append(char c) {
        if (len_ < N - 1) { buf_[len_++] = c; buf_[len_] = '\0'; }
    }

    const char* c_str() const { return buf_; }
    size_t size() const { return len_; }

private:
    char   buf_[N];
    size_t len_;
};

// Format an integer without printf
FixedString<16> itoa(int32_t v) {
    FixedString<16> s;
    if (v < 0) { s.append('-'); v = -v; }
    if (v == 0) { s.append('0'); return s; }
    char tmp[12]; int i = 0;
    while (v) { tmp[i++] = '0' + (v % 10); v /= 10; }
    while (i--) s.append(tmp[i]);
    return s;
}

What to use instead of the standard library

NeedFreestanding alternative
std::stringFixedString<N> or etl::string
std::vectorstd::array<T,N> or etl::vector
std::mapetl::map or sorted array with binary search
std::functionFunction pointer + void* context, or etl::delegate
std::unique_ptrManual RAII wrapper (no delete if using arena)
std::optionaletl::optional or manual struct with bool flag
LoggingRing buffer + UART write, no printf

The Embedded Template Library (ETL) provides STL-compatible containers without dynamic memory, and works in freestanding environments.

Edit on GitHubUpdated 2026-05-01T00:00:00.000Z