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
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]:
| Header | Available |
|---|---|
<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
# 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:
// 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
#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)
// 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
// 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
// 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:
#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
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
| Need | Freestanding alternative |
|---|---|
std::string | FixedString<N> or etl::string |
std::vector | std::array<T,N> or etl::vector |
std::map | etl::map or sorted array with binary search |
std::function | Function pointer + void* context, or etl::delegate |
std::unique_ptr | Manual RAII wrapper (no delete if using arena) |
std::optional | etl::optional or manual struct with bool flag |
| Logging | Ring buffer + UART write, no printf |
The Embedded Template Library (ETL) provides STL-compatible containers without dynamic memory, and works in freestanding environments.