A C++98 library reimplementing modern C++ standard library features – from C++11 all the way up to C++26.
My goal is to gain a deep understanding of how the standard library works behind the scenes. Doing it in C++98 originated out of necessity because my school (42) only allowed C++98 for our first projects in C++ (including an HTTP/1.1 webserver).
The things I chose to implement first were therefore mostly driven by what I could immediately use to make my code nicer and safer while learning the language in this constrained environment.
I learned to see the positives in this ordeal as I discovered the intricacies and quirks of C++, and I gained a deep appreciation for the newer standards, and compilers. How compilers parse and optimize some of this C++98 mess is mind-blowing to me (looking at you ft::is_convertible and ft::shared_ptr constructors).
I also discovered multiple bugs in gcc and clang along the way – luckily they all got fixed already in newer versions. But finding those bug reports and reading up on how they got fixed was a great learning experience. I left comments with links throughout the code wherever I encountered some.
Now it has turned into a fun challenge that often requires creative solutions – like custom move semantics. And wow is Boost impressive, doing all this so many years ago and so much better than I could come up with.
The first project I did at 42 was a C library reimplementing some C features called libft (lib-forty-two). I used this library throughout my studies there because we were not allowed to use most of the standard library. What we wanted to use we had to implement ourselves. I kept the name for this project as a nod to that.
C++98 only has lvalue references (&) – rvalue references (&&) were introduced in C++11, along with std::move. This library implements a Boost-inspired move emulation system that enables move-only and move-aware types in C++98.
The core idea: ft::rvalue<T> is a type that only rvalues (temporaries or explicit ft::move() casts) can bind to, exploiting how C++98 overload resolution works with derived-to-base conversions. Three macros configure a class for move semantics:
| Macro | Purpose |
|---|---|
FT_MOVABLE_BUT_NOT_COPYABLE(T) |
Move-only type (like std::unique_ptr) |
FT_COPYABLE_AND_MOVABLE(T) |
Copyable + movable with separate operators |
FT_COPYABLE_AND_MOVABLE_SWAP(T) |
Copyable + movable using copy-and-swap idiom |
ft::move() casts an lvalue to ft::rvalue<T>&, and ft::forward<T>() conditionally casts to an lvalue or rvalue emulation, providing reference-collapsing emulation.
A comprehensive <type_traits>-style header, built entirely with C++98 template metaprogramming – sizeof tricks, SFINAE, and partial specializations. (implementation)
Includes:
- Unary type traits –
is_void,is_integral,is_floating_point,is_array,is_function,is_pointer,is_lvalue_reference,is_rvalue_reference,is_arithmetic,is_object,is_reference,is_const,is_volatile,is_abstract,is_signed,is_unsigned,is_bounded_array,is_unbounded_array - Type relationships –
is_same,is_convertible - Type transformations –
remove_cv/add_cv,remove_const/add_const,remove_volatile/add_volatile,remove_reference,add_lvalue_reference/add_rvalue_reference,remove_extent/remove_all_extents,remove_pointer/add_pointer,remove_cvref,enable_if,conditional,voider(C++17'svoid_t) - Logical operations –
conjunction,disjunction,negation(emulated with up to 10 template parameters in place of variadic templates) - Property queries –
rank,extent - Base types –
integral_constant,bool_constant,true_type,false_type,type_identity - Custom traits –
is_class_or_union,is_complete,is_const_lvalue_reference,is_nonconst_lvalue_reference,is_returnable,has_member_function_swap(with a macroFT_HAS_MEMBER_FUNCTIONto generate member function detection traits)
A macro that emulates C++20's requires clauses, making SFINAE-constrained function signatures more readable:
// Instead of this:
template <typename T>
typename ft::enable_if<!ft::is_array<T>::value, ft::unique_ptr<T> >::type
make_unique();
// Write this:
template <typename T>
FT_REQUIRES(!ft::is_array<T>::value)
(ft::unique_ptr<T>) make_unique();ft::unique_ptr – Full implementation for both single objects and arrays, including custom deleters, make_unique, make_unique_for_overwrite, and full comparison operator sets. Uses the custom move semantics to enforce exclusive ownership. Supports constructing from std::auto_ptr when compiling under C++98.
ft::shared_ptr – Reference-counted shared ownership with type-erased deleters, aliasing constructors, pointer casts (static_pointer_cast, dynamic_pointer_cast, const_pointer_cast, reinterpret_pointer_cast), owner_less, owner_equal, and get_deleter. Supports array types and constructing from both ft::unique_ptr and std::auto_ptr.
Both smart pointer classes use ft::safe_bool for safe boolean conversions and FT_REQUIRES/enable_if to constrain constructors and conversions – resulting in some seriously dense template signatures.
Also provides ft::default_delete (with array specialization), ft::make_shared / ft::make_shared_for_overwrite, and ft::addressof.
ft::optional<T> – A C++17-style optional type with nullopt, has_value(), value(), value_or(), monadic operations (and_then, transform, or_else), and full comparison operators. Enforces constraints at compile time via FT_STATIC_ASSERT (no references, no arrays, no functions, no void, no nullopt_t).
ft::expected<T, E> – A C++23-style expected type for error handling without exceptions, with unexpected<E>, unexpect_t, has_value(), value(), error(), value_or(), error_or(), and monadic operations (and_then, transform, or_else, transform_error). Includes a void specialization for operations that can fail but don't return a value. Throws ft::bad_expected_access<E> on invalid access.
Note: Both currently use heap allocation internally. A future rework using discriminated union storage is planned (see Roadmap). The main challenge there is memory alignment.
| Component | Standard | Description |
|---|---|---|
ft::contains |
C++23 | Range and iterator search |
ft::contains_subrange |
C++23 | Subrange search |
ft::copy_if |
C++11 | Conditional copy |
ft::copy_n |
C++11 | Copy N elements |
ft::equal |
C++14 | Four-iterator overload for safe comparison |
ft::is_sorted / ft::is_sorted_until |
C++11 | Sorted range checks |
ft::lower_bound / ft::upper_bound |
— | Optimized overloads taking a precomputed size (avoids extra iteration for non-random-access iterators) |
ft::equal_range / ft::binary_search |
— | Same optimization as above |
ft::shift_left / ft::shift_right |
C++20 | Shift elements in a range |
ft::member_swap |
— | Generic swap preferring member swap() |
ft::iter_swap / ft::swap_ranges |
— | Iterator-based swap using ft::member_swap |
ft::min / ft::max |
— | Non-const overloads allowing assignment to the result |
Some functions also accept "ranges" – any type with begin()/end() and iterator typedefs, or bounded arrays.
| Component | Standard | Description |
|---|---|---|
ft::array<T, N> |
C++11 | Fixed-size aggregate container with iterators, element access, comparisons, fill, swap |
ft::get<I> |
C++11 | Compile-time indexed access |
ft::to_array |
C++20 | Create ft::array from a C-array |
| Component | Description |
|---|---|
FT_STATIC_ASSERT(expr) |
Compile-time assertion for C++98 |
ft::make_false<T...> |
Dependent false for unconditionally failing static assertions in templates |
ft::make_true<T...> |
Dependent true to suppress unused typedef warnings |
Wrappers around standard <cctype> functions that prevent undefined behavior by casting arguments to unsigned char first. Also returns bool and takes char for a cleaner interface.
| Component | Description |
|---|---|
ft::exception |
Base exception class with error(), where() (source location), and who() context fields |
All non-standard exception classes in the library inherit from ft::exception. Uses ft::source_location and ft::optional internally.
| Component | Standard | Description |
|---|---|---|
ft::expected<T, E> |
C++23 | Value-or-error container |
ft::expected<void, E> |
C++23 | Void specialization |
ft::unexpected<E> |
C++23 | Error wrapper |
ft::unexpect_t / ft::unexpect |
C++23 | In-place error construction tag |
ft::bad_expected_access<E> |
C++23 | Exception for invalid access |
| Component | Description |
|---|---|
ft::bold, ft::italic, ft::underline |
ANSI text styling |
ft::red, ft::green, ft::yellow, ft::blue, ft::magenta, ft::cyan, ft::gray |
ANSI color formatting |
ft::log::ok, ft::log::info, ft::log::warn, ft::log::error, ft::log::line |
Structured log-line formatters |
| Component | Standard | Description |
|---|---|---|
ft::function_traits<F> |
— | Query arity, return type, and argument types of function types (non-standard) |
ft::equal_to<T> |
C++14 | Transparent comparator with void specialization |
ft::not_equal_to<T> |
C++14 | Transparent comparator with void specialization |
ft::greater<T> |
C++14 | Transparent comparator with void specialization |
ft::less<T> |
C++14 | Transparent comparator with void specialization |
ft::greater_equal<T> |
C++14 | Transparent comparator with void specialization |
ft::less_equal<T> |
C++14 | Transparent comparator with void specialization |
| Component | Standard | Description |
|---|---|---|
ft::advance |
C++20 | Ranges-style overloads with sentinel bounds |
ft::next / ft::prev |
C++11 / C++20 | Iterator increment/decrement with sentinel overloads |
ft::begin / ft::end |
C++11 | Free function range access |
ft::cbegin / ft::cend |
C++14 | Const range access |
ft::rbegin / ft::rend |
C++14 | Reverse range access |
ft::crbegin / ft::crend |
C++14 | Const reverse range access |
ft::size / ft::ssize |
C++17/C++20 | Range size |
ft::empty |
C++17 | Range emptiness check |
ft::data |
C++17 | Direct access to underlying array |
ft::is_iterator |
— | Custom trait: check for valid iterator |
ft::is_input_iterator |
— | Custom trait: input iterator check |
ft::is_output_iterator |
— | Custom trait: output iterator check |
ft::is_forward_iterator |
— | Custom trait: forward iterator check |
ft::is_bidirectional_iterator |
— | Custom trait: bidirectional iterator check |
ft::is_random_access_iterator |
— | Custom trait: random access iterator check |
| Component | Standard | Description |
|---|---|---|
ft::unique_ptr<T, Deleter> |
C++11 | Exclusive-ownership smart pointer (single object + array specialization) |
ft::make_unique |
C++14 | Factory function (up to 10 args, emulating variadic templates) |
ft::make_unique_for_overwrite |
C++20 | Default-initializing factory |
ft::shared_ptr<T> |
C++11 | Reference-counted shared-ownership smart pointer |
ft::make_shared |
C++11 | Factory function (up to 10 args + array overloads) |
ft::make_shared_for_overwrite |
C++20 | Default-initializing factory |
ft::default_delete<T> |
C++11 | Default deleter (single object + array specialization) |
ft::static_pointer_cast |
C++11 | static_cast for shared_ptr |
ft::dynamic_pointer_cast |
C++11 | dynamic_cast for shared_ptr |
ft::const_pointer_cast |
C++11 | const_cast for shared_ptr |
ft::reinterpret_pointer_cast |
C++17 | reinterpret_cast for shared_ptr |
ft::get_deleter |
C++11 | Retrieve deleter from shared_ptr |
ft::owner_less |
C++11 | Owner-based ordering for shared_ptr |
ft::owner_equal |
C++26 | Owner-based equality for shared_ptr |
ft::addressof |
C++11 | Obtains actual address even if operator& is overloaded |
| Component | Description |
|---|---|
ft::rvalue<T> |
Rvalue reference emulation type |
ft::copy_assign_ref<T> |
Copy assignment wrapper for correct overload priority |
FT_MOVABLE_BUT_NOT_COPYABLE(T) |
Make a type move-only |
FT_COPYABLE_AND_MOVABLE(T) |
Make a type copyable and movable |
FT_COPYABLE_AND_MOVABLE_SWAP(T) |
Copyable and movable using copy-and-swap |
| Component | Standard | Description |
|---|---|---|
ft::numeric_cast<To>(from) |
Boost | Checked numeric conversion with overflow detection |
ft::add_sat / ft::sub_sat / ft::mul_sat / ft::div_sat |
C++26 | Saturating integer arithmetic |
ft::add_checked / ft::sub_checked / ft::mul_checked / ft::div_checked |
— | Checked arithmetic returning ft::expected |
ft::add_throw / ft::sub_throw / ft::mul_throw / ft::div_throw |
— | Throwing checked arithmetic |
ft::abs_diff |
— | Absolute difference without UB |
ft::iota |
C++11 | Fill range with incrementing values |
Both numeric_cast and from_string support a non-throwing overload via std::nothrow tag, returning ft::expected.
| Component | Description |
|---|---|
ft::operators::comparison |
Inherit from this and implement operator< to auto-generate ==, !=, >, <=, >= |
| Component | Standard | Description |
|---|---|---|
ft::optional<T> |
C++17 | Optional value container |
ft::nullopt_t / ft::nullopt |
C++17 | Empty optional sentinel |
ft::bad_optional_access |
C++17 | Exception for invalid access |
ft::make_optional |
C++17 | Factory function |
| Component | Description |
|---|---|
FT_CONCAT / FT_CONCAT_EXPANDED |
Token pasting with expansion |
FT_APPEND_UNIQUE_NUM(name) |
Generate unique identifiers using __COUNTER__ |
| Component | Description |
|---|---|
ft::urandom<T>() |
Read random data from /dev/urandom |
| Component | Description |
|---|---|
ft::safe_bool<Derived> |
CRTP mixin implementing the Safe Bool idiom – prevents implicit conversions to int and cross-type comparisons |
ft::safe_bool<void> |
Virtual dispatch variant for polymorphic hierarchies |
Needed because C++98 lacks explicit operator bool(). Used by unique_ptr, shared_ptr, optional, and expected.
| Component | Standard | Description |
|---|---|---|
ft::source_location |
C++20 | Captures file name, line, and function name |
FT_SOURCE_LOCATION_CURRENT() |
C++20 | Macro to capture current source location |
| Component | Standard | Description |
|---|---|---|
ft::starts_with |
C++20 | String prefix check |
ft::ends_with |
C++20 | String suffix check |
ft::from_string<To> |
— | String-to-value parsing with format flags and error handling |
ft::to_string |
C++11 | Value-to-string conversion with format flags |
ft::trim |
— | Whitespace trimming |
from_string provides both throwing and non-throwing (std::nothrow tag → ft::expected) overloads with detailed error types (from_string_invalid_exception, from_string_range_exception).
See Type Traits for the full list.
Also provides:
FT_REQUIRES(expr)– requires clause emulationFT_HAS_MEMBER_FUNCTION(ret, name, args)– generate traits detecting public member functionsft::yes_type/ft::no_type– helper types for SFINAEsizeoftricks
| Component | Standard | Description |
|---|---|---|
ft::move |
C++11 | Cast to rvalue reference emulation |
ft::forward |
C++11 | Perfect forwarding (reference collapsing emulation) |
ft::nullptr_t / FT_NULLPTR |
C++11 | nullptr emulation as a class + macro |
ft::demangle |
— | Demangle typeid names (non-standard) |
FT_UNREACHABLE() |
C++23 | Marks unreachable code paths |
FT_COUNTOF(arr) |
— | Type-safe compile-time array length |
Things I would love to work on next:
-
ft::variantand compile-time type-list – discriminated union, the foundation for stack-allocated polymorphic storage - Rework
ft::optionalandft::expected– eliminate heap allocations using aligned storage and placementnew, applying knowledge gained fromft::variant -
ft::function– type-erased callable wrapper -
ft::reference_wrapper– reference semantics in value contexts -
ft::unordered_*containers – hash-based containers -
ft::string_view– non-owning string reference - Ranges including range-based algorithms
The biggest mistake I made was to not set up a proper testing framework. Now I have lots of things that have no tests, and the test files I have are so ugly and made in different and inconsistent styles that I don't even dare to have them on the main branch. I truly learned the importance of testing in this project – through regret.
makeThis produces libftpp.a. Link against it and add the project root to your include path.
-
Public headers live in the
libftpp/directory. -
All symbols are in the
ft::namespace and mirrorstd::interfaces as closely as C++98 allows. -
Public macros always start with
FT_. -
Most symbols have doxygen-style comments linking either to the cppreference.com page if they behave like the stdlib version, or providing notes about their usage, behavior, and limitations.