.png&w=3840&q=75)
let’s talk about some essential concepts before diving into the actual implementation of containers :
disclaimer: there are some oversimplifications that might alter the meaning of some details please refer to the resources for more deep understanding
1 - exception safety and RAII
- resources
- https://en.wikipedia.org/wiki/Call_stack#Unwinding
- https://scvgoe.github.io/2019-07-06-Exception-Safety/
- https://www.boost.org/community/exception_safety.html
so what is exception safety?
before I answer that I should first introduce to you an important concept which is stack unwinding
stack unwinding :
when a function is called, information about that routine gets stacked up on the call stack inside a frame. Each function(subroutine) gets its own frame with whatever information instruction needs the CPU to execute that function.
now when the function finishes its execution, its frame gets destroyed and the CPU executes the next function in a FILO order
When an exception occurs while a function is in progress on the stack, it returns to the call stack by finding the catch block, it just keeps returning through the execution path. resulting in the destruction of the function frame.
if there is any memory allocated before the exception throw, memory leaks will occur


void f3() {
int *c = new int[100]; // will be leak
std::string s = "this is stack... would be destroyed";
throw "exception!";
}
void f2() { f3() }
void f1() { try { f2() } catch (...) { std::cout << "back to here!\n"; } }• f1->f2->f3Stacks are stacked in f3->f2->f1order, and stacks are released in order.
- the exception will follow that execution path until it finds the catch block, destroying function frames without giving a damn about allocated memory Levels of exception safety : now that we know what exception safety is let us discover the levels of exception safety and at what level our code should be exception safe Exception safety has four levels. (sort by safe order)How to: Design for Exception Safety In C++
- No-throw guarantee (failure transparency): At a level that guarantees the success and safety of all operations, when an exceptional situation occurs, it is handled internally, and success and stability are guaranteed without being visible to clients.
- Strong exception safety (commit or rollback semantics): The operation may fail, but it is guaranteed that the failed operation does not cause other side effects. Therefore, all data retains its original values.
- Basic exception safety (no-leak guarantee): Although some actions of a failed operation can cause side effects, all invariants are always preserved and no resource leaks including memory leaks are guaranteed. The existing data stored may change its value, but it will still have a valid value.
- No exception safety : a level in which nothing is guaranteed. In general, a level of at least basic exception safety is required for a robust code. A high level of safety is difficult to achieve and may incur overhead due to additional copies. The key to exception safety is to ensure that program execution continues even after a block of code (even if it is an exception) is executed. Some languages make this simple by using the dispose pattern (with, try-with-resources).
- Keep Resource Classes Simple : When encapsulating a manual resource in a class, you should do nothing but manage the resource. Also, it is better to use a smart pointer if possible.
- Use the RAII Idiom to Manage Resources : RAII (Resource Acquisition Is Initialization) Idiom is a design pattern proposed by Bjarne Stroustrup, the creator of C++. The RAII pattern is an important technique to prevent leaks in languages such as C++ where the developer needs to directly manage the resource. It releases the resource automatically when the scope of use of the resource ends, and the resource is acquired even when an exception occurs. It must be ensured that this
RAII (Resource Acquisition Is Initialization)
- resources https://www.stroustrup.com/3rd_safe.pdf https://occamsrazr.net/tt/297 https://en.cppreference.com/w/cpp/language/raii
Resource Acquisition is Initialization is a c++ technique that ensures that the allocated resources will be released
it consists of creating an object that will encapsulate and manage the resource Acquisition/allocation in the constructor. ****and release that resource in the destructor.
- std::unique_ptr and std::shared_ptr to manage dynamically-allocated memory …</aside>
- Functors (Function Objects):
- resources MS functor vs lambda expression what are c++ functors and their uses (StackOverflow 따봉 1000개) about functor function object Wikipedia
- As the name shows, Functor is a combination of a function and an object.
- it's an object that behaves like a function. and that’s by overloading the () operator
- functors are widely used in STL algorithms
struct Add{
Add(int toAdd) : _toAdd(toAdd) {}
int operator()(int x){
return x + _toAdd;
}
private:
int _toAdd;
};
int main(){
Add add_42(42); // state
Add add_13(13);
int result_42 = add_42(2);
int result_13 = add_13(2);
std::vector<int> v(10, 10);
std::transform(v.begin(), v.end(), v.begin(), add_42);
}Classification of functors
- generator: a functor with no arguments
- unary: a functor that takes one argument
- binary: a functor that takes two arguments
- predicate: Used as a functor that returns a boolean value, Unary predicate, Binary predicate, etc.
- operator: a functor that returns an operation value
functor vs function
- A functor is an object that operates like a function by overloading the () operator on the object.
- Because it is an object, it can be passed as an argument and can be used in the form of a callback.
- Because it is a callback type, it is compatible with STL's algorithm.
- the state can be stored
- To pass a function as an argument to a specific function, you need to do something else (function pointer or function object).
functor vs function pointer
- A functor has a clearly defined type.
- Therefore, it can also be used as a template argument.
- Optimization is possible in the compilation stage, so it can be inlined.
- It can store state and can also have regular member variables/functions.
- A large amount of code is required to create a structure or class.
- A function pointer can contain other functions as long as the type is the same.
- It is not determined at compile time, but the function is determined at runtime, so overhead occurs and cannot be inlined.
- The state cannot be saved.
function vs lambda expression (c++11)
Both can be inlined and lambda expression defaults to inline.
- A lambda expression is similar to a functor, but it is simpler because there is no need to define a class. Feels like syntax sugar
2- SFINAE
- SFINAE (Substitution Failure Is Not An Error)
before we define what SFINAE really is, let's first see how the compiler chooses the right function overload to call.
in c++, the compiler goes through a process in order for it to find the right function overload.
these steps are:
- 1 name lookup in this step, the compiler looks for candidate functions that have the same name this step consists of two different types of lookup :
- • unqualified name lookup: to put it simply, in this type of lookup, there’s a search for the namespace, if there’s one, otherwise we check in the global namespace;
- • qualified name lookup: in this type of lookup, we search more specifically for the function name in the namespace that we found in the unqualified name lookup
- 2 • ADL (Argument Dependent Lookup) :
- ADL is the set of rules for looking up the unqualified function names in function-call expressions, including implicit function, calls to overloaded operators.
- the argument-dependent lookup is not considered if the lookup set produced by the usual unqualified lookup contains any of the following: 1) a declaration of a class member 2) a declaration of a function at block scope (that's not a using-declaration) 3) any declaration that is not a function or a function template (e.g. a function object or another variable whose name conflicts with the name of the function that's being looked up)
- for every argument in a function call expression, its type is examined to determine the associated set of namespaces and classes that it will add to the lookup. <aside> 💡 read more here : [https://en.cppreference.com/w/cpp/language/adl](https://en.cppreference.com/w/cpp/language/adl) </aside>
- For arguments of fundamental type, the associated set of namespaces and classes is empty
- For arguments of class type (including union) …
- For arguments whose type is a class template specialization …
- For arguments of enumeration type …
- For arguments of type pointer to T or pointer to an array of T …
- For arguments of function type, the function parameter types and the function return type are examined and their associated set of classes and namespaces are added to the set.
- For arguments of type pointer to member function F of class X …
- For arguments of type pointer to data member T of class X …
- ….
- 3 template argument deduction
- at this point, Template functions are untyped and need to be instantiated.
- A template function is instantiated, either explicitly or implicitly, but not always all arguments of the template are determined.
- At this point, if possible, the compiler deduces the template argument.
- In this way, a candidate function set is created, and more than one function can be a candidate.
4 • template argument substitution
- 7 - Substitute function arguments in the list, but if there is a problem with the type or expression, the substitution fails
- 8 - When substitution fails, it does not generate a compile error and works by excluding the candidate function from the candidate group! (SFINAE)
- 5 - overload resolution
- Find the function actually called through overload resolution!
- At this time, the function candidates are
candidate functionscalled, and the function actually calledviable functionis called!
Specifically, when creating a candidate set for overload resolution, some (or all) candidates of that set may be the result of instantiated templates with (potentially deduced) template arguments substituted for the corresponding template parameters. If an error occurs during the substitution of a set of arguments for any given template, the compiler removes the potential overload from the candidate set instead of stopping with a compilation error, provided the substitution error is one the C++ standard grants such treatment.[2] If one or more candidates remain and overload resolution succeeds, the invocation is well-formed.
in the next section, we will see how we can use SFINAE to our advantage, through type traits, and tag dispatching ….
3- Type Traits / Tag dispatching …
Type Traits :
- resources A quick primer on type traits in modern C++ https://dev.to/sandordargo/what-are-type-traits-in-c-18j5 https://cs.brown.edu/~jwicks/boost/libs/type_traits/cxx_type_traits.htm
so what are type traits?
let's start with what is a trait.
a trait is "a particular characteristic that can produce a particular type of behavior". Or simply "a characteristic, especially of a personality".
we can think about type traits as properties of a type .
you'd often need the information on what kind of types are accepted by a template, and what types are supported by certain operations. While concepts are much superior in terms of expressiveness or usability, with type traits you could already introduce compile-time conditions on what should be accepted as valid code and what not.
Though type traits can help with even more. With their help, you can also add or remove the const specifier, you can turn a pointer or a reference into a value, and so on….
Type traits are a clever technique used in C++ template metaprogramming that gives you the ability to inspect and transform the properties of types.
For example, given a generic typeT— it could beint,bool,std::vector, or whatever you want — with type traits you can ask the compiler some questions: is it an integer? Is it a function? Is it a pointer? Or maybe a class? Does it have a destructor? Can you copy it? Will it throw exceptions? ... and so on. This is extremely useful in conditional compilation, where you instruct the compiler to pick the right path according to the type in input. We will see an example shortly.
Tag Dispatching :
sometimes in metaprogramming, we want our template function to behave differently depending on the types given as parameters
we can achieve that in different means :
- there’s **Conditional compilation, which** helps to compile a specific portion of the program or lets us skip the compilation of some specific part of the program based on some conditions
- using type-traits (is_integral …)
- more simply, we can make a function that takes an option number(flag) , and depending on that number executes a different code.
- or we can use Tag Dispatching:
Tag-dispatching is a metaprogramming technique to overload a function using different tag parameters. The right overload is chosen by a compiler at compile time
- it works like this:
by creating several tags (so several types), we can use them to route the execution through various overloads of a function.
template<class It>
It plus2_impl(It it, std::forward_iterator_tag)
{
++it; ++it;
return it;
}
template<class It>
It plus2_impl(It it, std::random_access_iterator_tag)
{
return it + 2;
}
template<class It>
It plus2(It it)
{
using Tag = typename std::iterator_traits<It>::iterator_category;
return plus2_impl(it, Tag{});
}- is_integral :
template <class T> struct is_integral;

is_integral is a Trait class that identifies whether T is an integral type.
it inherits from the class integral_constant either TRUE or FALSE depending on the type is it an integral type or not
here are the integral types :
bool , char ,char16_t , char32_t , wchar_t , signed char,short int, int
long int,long long int , unsigned char , unsigned short int , unsigned int
unsigned long int, unsigned long long int- enable_if :
- resources https://leimao.github.io/blog/CPP-Enable-If/ https://h-deb.clg.qc.ca/Sujets/TrucsScouts/Comprendre_enable_if.html https://medium.com/@sidbhasin82/c-templates-what-is-std-enable-if-and-how-to-use-it-fd76d3abbabe
The enable_if trait is a technique to control the application of SFINAE.
In C++ metaprogramming, std::enable_if is an important function to enable certain types for template specialization via some predicates known at the compile time. Using types that are not enabled by std::enable_if for template specialization will result in a compile-time error.
std::enable_if can be used in many forms, including:
- as an additional function argument (not applicable to operator overloads)
- as a return type (not applicable to constructors and destructors)
- as a class template or function template parameter</aside>
implementation of enable_if :
template<bool B, class T = void>
struct enable_if {};
template<class T>
struct enable_if<true, T> { typedef T type; };- **lexicographical_compare :**
lexicographical comparison is the kind of comparison generally used to sort words alphabetically in dictionaries; It involves comparing sequentially the elements that have the same position in both ranges against each other until one element is not equivalent to the other. The result of comparing these first non-matching elements is the result of the lexicographical comparison.
- The behavior of this function template is equivalent to:
template <class InputIterator1, class InputIterator2>
bool lexicographical_compare (InputIterator1 first1, InputIterator1 last1,
InputIterator2 first2, InputIterator2 last2)
{
while (first1!=last1)
{
if (first2==last2 || *first2<*first1) return false;
else if (*first1<*first2) return true;
++first1; ++first2;
}
return (first2!=last2);
}- std::pair **:**
the pair is pretty strait forward This class couples together a pair of values, which may be of different types (T1 and T2). The individual values can be accessed through its public members first and second.
- the make_pair is just a function that returns a pair.
3- Iterators :
- so what are iterators, and how can we implement them?
An iterator is an object (like a pointer) that points to an element inside the container. We can use iterators to move through the contents of the container. They can be visualized as something similar to a pointer pointing to some location and we can access the content at that particular location using them. Iterators play a critical role in connecting algorithms with containers along with the manipulation of data stored inside the containers.
in c++ there are 5 categories of iterators, each with its own properties that we should know in order to work with them efficiently

- Iterator Traits :
- resources https://cplusplus.com/reference/iterator/iterator_traits/ https://www.fluentcpp.com/2018/05/08/std-iterator-deprecated/ https://www.codeproject.com/Articles/36530/An-Introduction-to-Iterator-Traits https://stackoverflow.com/questions/6742008/what-are-the-typical-use-cases-of-an-iterator-trait https://pratikparvati.com/html/blogview.html?id=-MiCLwwkhOtKkANlNWje&lan=cpp
Generic code that uses iterators, such as the STL algorithms which use them intensely, needs information about them. For example, it needs the type of object that the iterators refer to.
so how does it gets this information?
Another example of the required information is the capabilities of the iterator: is it just an input iterator, that supports++but should not be read twice? Or a forward iterator that can be read several times? Or a bidirectional that can also do--? Or a random access iterator, that can jump around with+=,+,-=, and-? Or an output iterator?
This piece of information is useful for some algorithms that would be more or less efficient depending on those capabilities. Such an algorithm typically has several implementations and chooses one to route to depending on the category of the iterator.
Iterator Traits is a class trait that provides specific information about the iterator that stl::algorithms needs
| member | description |
|---|---|
| difference_type | Type to express the result of subtracting one iterator from another. it gets stored in type able to represent the result of any valid pointer subtraction operation(ptrdiff_t) |
| value_type | The type of element the iterator can point to. |
| pointer | The type of a pointer to an element the iterator can point to. |
| reference | The type of reference to an element the iterator can point to. |
| iterator_category | The iterator category. It can be one of these: |
| • input_iterator_tag | |
| • output_iterator_tag | |
| • forward_iterator_tag | |
| • bidirectional_iterator_tag | |
| • random_access_iterator_tag |
The iterator_traits class template comes with a default definition that obtains these types from the iterator type itself (iterator class). It is also specialized for pointers (T*) and pointers to const (const T*).Note that any custom class will have a valid instantiation of iterator_traits if it publicly inherits the base class std::iterator.
Member types
| member | generic definition | T* specialization |
const T* specialization |
|---|---|---|---|
| difference_type | Iterator::difference_type | ptrdiff_t | ptrdiff_t |
| value_type | Iterator::value_type | T | T |
| pointer | Iterator::pointer | T* | const T* |
| reference | Iterator::reference | T& | const T& |
| iterator_category | Iterator::iterator_category | random_access_iterator_tag | random_access_iterator_tag |
- the Iterator Class :
- resources https://cplusplus.com/reference/cstddef/ptrdiff_t/ https://cplusplus.com/reference/iterator/iterator/ https://cplusplus.com/reference/iterator/
std::iterator is a helper to define the iterator traits of an iterator.
std::iterator is a template, that takes 5 template parameters:
template<
typename Category,
typename T,
typename Distance = std::ptrdiff_t,
typename Pointer = T*,
typename Reference = T&
> struct iterator;Those 5 names sound familiar, right? Those template types correspond to the 5 types required by the STL on iterators.
The job of std::iterator is to expose those types. Here is one possible implementation of std::iterator:
template<
typename Category,
typename T,
typename Distance = std::ptrdiff_t,
typename Pointer = T*,
typename Reference = T&
> struct iterator
{
using iterator_category = Category;
using value_type = T;
using difference_type = Distance;
using pointer = Pointer;
using reference = Reference;
};std::iterator allows an iterator to define these 5 types, by inheriting from std::iterator and passing it those types (at least the first 2 since the other 3 have default values):
class MyIterator : public std::iterator<std::random_access_iterator, int>
{
// ...
};By inheriting from std::iterator, MyIterator also exposes the 5 types.
4 - Allocator :
- resources https://en.wikipedia.org/wiki/Allocator_(C%2B%2B) https://medium.com/@terselich/1-a-guide-to-implement-a-simple-c-stl-allocator-705ede6b60e4 https://www.enseignement.polytechnique.fr/informatique/INF478/docs/Cpp/en/cpp/concept/Allocator.html https://www.enseignement.polytechnique.fr/informatique/INF478/docs/Cpp/en/cpp/memory/allocator_traits.html https://hackingcpp.com/cpp/design/allocators.html https://stackoverflow.com/questions/5628059/c-stl-allocator-vs-operator-new https://stackoverflow.com/questions/4254811/memory-management-stdallocator https://stackoverflow.com/questions/4254811/memory-management-stdallocator https://stackoverflow.com/questions/31358804/whats-the-advantage-of-using-stdallocator-instead-of-new-in-c
an Allocator is an object that’s responsible for encapsulating memory management
every stl container except (std::array, tostd::shared_ptrandstd::function)
has an allocator that Encapsulates memory allocation and deallocation strategy.
- you might ask yourself a question, what about the new and delete ? what’s wrong with them ?
if you are coming from a c language background you may ask why not malloc or alloc instead of new and delete? , so let’s first answer this
malloc() is a library function of stdlib.h and it was used in C language to allocate memory for N blocks at run time, it can also be used in C++ programming language. Whenever a program needs memory to declare at run time we can use this function.
while new is an operator in C++ programming language, it is also used to declare memory for N blocks at run time.
here are some differences between malloc and new :
now time to answer why using an allocator and not new and delete :
the std::allocator was not created to replace new and delete.
in fact, the allocator uses new and delete in its internals.
what’s so special about the allocator is that it separates the allocation and construction, deallocation and destruction.
for general programming using new and delete is more than enough, but when creating a container it's a must to take control of these things.
here is an example :
std::vector<X> v;
v.reserve(4); // (1)
v.push_back( X{} ); // (2)
v.push_back( X{} ); // (3)
v.clear(); // (4)
- let’s deep dive into the std::allocator and how it works :
- allocator_traits :
just like the iterator, the std::allocator needs to have some information about the container in order for it to work properly.
Theallocator_traitsthe class template provides a standardized way to access various properties of allocators. The standard containers and other standard library components access allocators through this template, which makes it possible to use any class type as an allocator, as long as the user-provided specialization ofallocator_traitsimplements all required functionality.
The default, non-specialized, allocator_traits
contains the following members:
Member types
| Type | Definition |
|---|---|
allocator_type |
Alloc |
value_type |
Alloc::value_type |
pointer |
Alloc::pointer if present, otherwise value_type* |
const_pointer |
Alloc::const_pointer if present, otherwise std::pointer_traits |
void_pointer |
Alloc::void_pointer if present, otherwise std::pointer_traits |
const_void_pointer |
Alloc::const_void_pointer if present, otherwise std::pointer_traits |
difference_type |
Alloc::difference_type if present, otherwise std::pointer_traits |
size_type |
Alloc::size_type if present, otherwise std::make_unsigned |
propagate_on_container_copy_assignment |
Alloc::propagate_on_container_copy_assignment if present, otherwise std::false_type |
propagate_on_container_move_assignment |
Alloc::propagate_on_container_move_assignment if present, otherwise std::false_type |
propagate_on_container_swap |
Alloc::propagate_on_container_swap if present, otherwise std::false_type |
is_always_equal(since C++17) |
Alloc::is_always_equal if present, otherwise std::is_empty |
the template std::allocator_traits
supplies the default implementations for all optional requirements, and all standard library containers and other allocator-aware classes access the allocator through std::allocator_traits
, not directly.Member functions
| allocate [static] | allocates uninitialized storage using the allocator(public static member function) |
|---|---|
| deallocate [static] | deallocates storage using the allocator(public static member function) |
| construct [static] | constructs an object in the allocated storage(function template) |
| destroy [static] | destructs an object stored in the allocated storage(function template) |
| max_size [static] | returns the maximum object size supported by the allocator(public static member function) |
| select_on_container_copy_construction [static] | obtains the allocator to use after copying a standard container(public static member function) |
with this in mind, I think we have enough knowledge to try and deep dive into the
re-implementation of the containers.
FT_VECTOR :
- resources [https://medium.com/@vgasparyan1995/how-to-write-an-stl-compatible-container-fc5b994462c6#:~:text=For insertion,time (O(1)](https://medium.com/@vgasparyan1995/how-to-write-an-stl-compatible-container-fc5b994462c6#:~:text=For%20insertion%20it,time%20(O(1)) https://stackoverflow.com/questions/6296945/size-vs-capacity-of-a-vector#:~:text=Size is not, what they want https://www.geeksforgeeks.org/how-to-implement-our-own-vector-class-in-c/ https://stackoverflow.com/questions/61889111/own-vector-assign-implementation
- prototype :
template < class T, class Alloc = allocator<T> > class vector;
The iterators of vector are of the “random access” category:→ Random-access iterators are iterators that can be used to access elements at an arbitrary offset position relative to the element they point to, offering the same functionality as pointers.
→ Random-access iterators are the most complete iterators in terms of functionality. All pointer types are also valid random-access iterators.
Member types
| Member type | Definition |
|---|---|
value_type |
T |
allocator_type |
Allocator |
size_type |
Unsigned integer type (usually std::size_t) |
difference_type |
Signed integer type (usually std::ptrdiff_t) |
reference |
value_type& |
const_reference |
const value_type& |
pointer |
Allocator::pointer(until C++11)std::allocator_traits |
const_pointer |
Allocator::const_pointer(until C++11)std::allocator_traits |
iterator |
LegacyRandomAccessIterator and LegacyContiguousIterator to value_type(until C++20)LegacyRandomAccessIterator, contiguous_iterator, and ConstexprIterator to value_type(since C++20) |
const_iterator |
LegacyRandomAccessIterator and LegacyContiguousIterator to const value_type(until C++20)LegacyRandomAccessIterator, contiguous_iterator, and ConstexprIterator to const value_type(since C++20) |
reverse_iterator |
std::reverse_iterator |
const_reverse_iterator |
std::reverse_iterator |
Member functions
| (constructor) | constructs the vector(public member function) |
|---|---|
| (destructor) | destructs the vector(public member function) |
| operator= | assigns values to the container(public member function) |
| assign | assigns values to the container(public member function) |
| get_allocator | returns the associated allocator(public member function) |
| Element access | |
| at | access specified element with bounds checking(public member function) |
| operator[] | access specified element(public member function) |
| front | access the first element(public member function) |
| back | access the last element(public member function) |
| data | direct access to the underlying array(public member function) |
| Iterators | |
| begin cbegin(C++11) | returns an iterator to the beginning(public member function) |
| endcend(C++11) | returns an iterator to the end(public member function) |
| begin crbegin(C++11) | returns a reverse iterator to the beginning(public member function) |
| rend crend(C++11) | returns a reverse iterator to the end(public member function) |
| Capacity | |
| empty | checks whether the container is empty(public member function) |
| size | returns the number of elements(public member function) |
| max_size | returns the maximum possible number of elements(public member function) |
| reserve | reserves storage(public member function) |
| capacity | returns the number of elements that can be held in currently allocated storage(public member function) |
| Modifiers | |
| clear | clears the contents(public member function) |
| insert | inserts elements(public member function) |
| erase | erases elements(public member function) |
| push_back | adds an element to the end(public member function) |
| pop_back | removes the last element(public member function) |
| resize | changes the number of elements stored(public member function) |
| swap | swaps the contents(public member function) |
Non-member functions
| operator==operator!=operator<operator<=operator>operator>=operator<=> | lexicographically compares the values in the vector(function template) |
|---|---|
| std::swap(std::vector) | specializes the std::swap algorithm(function template) |
FT_STACK :
- resources https://en.cppreference.com/w/cpp/container/stack https://stackoverflow.com/questions/3873802/what-are-containers-adapters-c#:~:text=values in it.-,Container Adapters,-Container adapters%2C on
The std::stack class is a container adapter. Container objects hold data of a similar data type. You can create a stack from various sequence containers. If no container is provided, the deque contains will be used by default. Container adapters don’t support iterators, so they can’t be used to manipulate data.
the std::stack is built on top of the std::vector as an underlying container
so most of its member functions are just a call to the underlying container’s member functions
Member types
| Member type | Definition |
|---|---|
container_type |
Container |
value_type |
Container::value_type |
size_type |
Container::size_type |
reference |
Container::reference |
const_reference |
Container::const_reference |
Member functions
| (constructor) | constructs the stack(public member function) |
|---|---|
| (destructor) | destructs the stack(public member function) |
| operator= | assigns values to the container adaptor(public member function) |
| Element access | |
| top | accesses the top element(public member function) |
| Capacity | |
| empty | checks whether the underlying container is empty(public member function) |
| size | returns the number of elements(public member function) |
| Modifiers | |
| push | inserts element at the top(public member function) |
| pop | removes the top element(public member function) |
| swap(C++11) | swaps the contents(public member function) |
Member objects
| Container c | the underlying container(protected member object) |
|---|
Non-member functions
| operator==operator!=operator<operator<=operator>operator>=operator<=> | lexicographically compares the values in the stack(function template) |
|---|---|
| std::swap(std::stack) | specializes the std::swap algorithm(function template) |
RED-BLACK-TREE
- resources https://www.youtube.com/watch?v=nMExd4DthdA&list=PLOXdJ6q8iu4MneI9gySCHiyzAQcveqkIO https://www.programiz.com/dsa/red-black-tree https://jojozhuang.github.io/algorithm/data-structure-red-black-tree/ https://www.happycoders.eu/algorithms/red-black-tree-java/ https://www.codesdope.com/course/data-structures-red-black-trees-insertion/

Problems with Binary Search Tree :
For Binary Search Tree, although the average time complexity for the search, insertion and deletion are all O(log n), where n is the number of nodes in the tree, the time complexity becomes O(n) in the worst case - BST is not balanced.

We can guarantee O(logn) time for all three operations by using a balanced tree - a tree that always has height log(n).
so, what is a red-black tree?
A red-black tree is a self-balancing binary search tree, that is, a binary search tree that automatically maintains some balance.
Red-black trees are an evolution of binary search trees that aim to keep the tree balanced without affecting the complexity of the primitive operations. This is done by coloring each node in the tree with either red or black and preserving a set of properties that guarantee that the deepest path in the tree is not longer than twice the shortest one.
Red-Black Tree Properties
The following rules enforce the red-black tree balance:
- Each node is either red or black.
- (The root is black.)
- All NIL leaves are black.
- A red node must not have red children.
- All paths from a node to the leaves below contain the same number of black nodes.
upper_bound/lower_bound:
The names already have an intuitive but different meaning in mathematics. Given the mathematical meanings, it seems weird that in a big set or map, upper_bound and lower_bound are actually the same or adjacent elements.
the basic rules you have to remember:
lower_boundReturns an iterator pointing to the first element that is not less than (i.e. greater or equal to)keyupper_boundReturns an iterator pointing to the first element that is greater thankey
predecessor and successor:
When you do the in-order traversal of a binary tree, the neighbors of a given node are called Predecessor(the node lies behind of given node) and Successor (the node lies ahead of the given node).
here’s an example :

Approach:
Say you have to find the in-order predecessor and successor node 15.
- First, compare the 15 with the root (25 here).
- 25>15 => successor = 25, make a recursive call to root. left.(Why we do it, is explained in step 3).
- The new root which is = 15, now stops making recursive calls.
- Now successor will be the leftmost node in the right subtree which has the root 18 here. So the leftmost element and successor will be 19. (What if 15 doesn’t have a right subtree, then the successor of 15 will be the parent of it, and that is why we set the parent as successor in step 1).
- Now predecessor will be the rightmost node in the left subtree which has the root 10 here. but 10 doesn’t have the right child.
- For the same reason when node<root then make predecessor = root and make a recursive call to the root.right.
pseudocode :
if (root != null) {
if (root.data == val) {
// go to the right most element in the left subtree, it will the
// predecessor.
if (root.left != null) {
Node t = root.left;
while (t.right != null) {
t = t.right;
}
predecessor = t.data;
}
if (root.right != null) {
// go to the left most element in the right subtree, it will
// the successor.
Node t = root.right;
while (t.left != null) {
t = t.left;
}
successor = t.data;
}
} else if (root.data > val) {
// we make the root as successor because we might have a
// situation when value matches with the root, it wont have
// right subtree to find the successor, in that case we need
// parent to be the successor
successor = root.data;
successorPredecessor(root.left, val);
} else if (root.data < val) {
// we make the root as predecessor because we might have a
// situation when value matches with the root, it wont have
// left subtree to find the predecessor, in that case we need
// parent to be the predecessor.
predecessor = root.data;
successorPredecessor(root.right, val);
}
}
}red-black tree iterator :
Iterators for tree-based data structures can be more complicated than those for linear structures.
For arrays (and vectors and deques and other array-like structures) and linked lists, a single pointer can implement an iterator:
- Given the current position, it is easy to move forward to the next element.
- For anything but a singly-linked list, we can also easily move backward.

Iterating over Trees:
look at this binary search tree, and suppose that you were implementing tree iterators as a single pointer. Let’s see if we can “think” our way through the process of traversing this tree, one step at a time, without needing to keep a whole stack of unfinished recursive calls around.

We’re going to try to visit the nodes in the same order we would process them during an “in-order” traversal, which, for a BST, means that we will visit the data in ascending order.
It’s not immediately obvious what our data structure for storing our “current position” (i.e., an iterator) will be. We might suspect that a pointer to a tree node will be part or the whole of that data structure, in only because that worked for us with iterators over linked lists. With that in mind, …
begin(),end():
- now, how would we implement the begin() ?
- which node is the first in an in-order traversal? We find the
begin()position by starting from the root and working our way down, always taking left children until we come to a node with no left child. - the same thing applies to the end() but : it's by returning a null pointer.
It’s tempting to guess that you could do much the same as for
begin(), this time seeking out the rightmost node. But that would leave you pointing to the last node in the tree, andend(), for any container, is supposed to denote the position after the last element in the container.
operator++:
Suppose you are still trying to implement iterators using a single pointer, you have one such pointer named current as shown in the figure.
Question: How would you implement ++current?
Well, we know that we should wind up at node 50. But how can we get there?

We can’t, not with just a pointer to the node and all the nodes pointing only to their children. The only place you can go within this tree is down, and there is no “down” from our current position.
In a binary tree, to do operator++ : • We need to know not only where we are, • but also how we got here.
One way to do that is to implement the iterator as a stack of pointers containing the path to the current node. In essence, we would use the stack to simulate the activation stack during a recursive traversal. But that’s pretty clumsy. Iterators tend to get assigned (copied) a lot, and we’d really like that to be an O(1) operation. Having to copy an entire stack of pointers just isn’t very attractive.
now let’s try to figure out just what our code should do :
- Question: Suppose that we are currently at node E. What is the in-order successor (the node that comes next during an in-order traversal) of E? Answer: G is the in-order successor of E. (If you answered F, remember that in an in-order traversal, we visit a node only after visiting all of its left descendants and before visiting any of its right descendants. Since we’re at E, we must have already visited F.) That example suggests that a node’s in-order successor tends to be among its right descendants.

Question: Suppose that we are currently at node A. What is the in-order successor (the node that comes next during an in-order traversal) of A?
Answer: F is the in-order successor of A. If we are at A during an in-order traversal, we have already visited all of A’s left descendants. So the answer has to be C or one of its descendants. It’s tempting to pick C because it’s only one step away from A. But, remember, during an in-order traversal, we visit a node only after visiting all of its left descendants and before visiting any of its right descendants. We have not yet visited C’s left descendants. So have to run down from C to the left as far as we can go.
This suggests that, if a node has any right descendants, we should
• Take a step down to the right, then • Run as far down to the left as we can.
You can see how this would take us from A to F. And, for that matter, it would take us from E to G as well. So both of our prior examples are satisfied.
- Question: Suppose that we are currently at node C. What is the in-order successor of C? Answer: C does not have an in-order successor. C is actually the final node in an in-order traversal. After C is only
end(). OK, that’s an interesting special case, but it doesn’t make clear what should happen in the more general case where we have no right child.

- Question: What is the in-order successor of F? Answer: E is the in-order successor of F. So, when we have no right child, we may need to move back up in the tree.
- Question: What is the in-order successor of G? Answer: C is the in-order successor of G.
To summarize : • If the current node has a non-null right child, ◦ Take a step down to the right ◦ Then run down to the left as far as possible • If the current node has a null right child, ◦ move up the tree until we have moved over a left child link
with that in mind lets move to pseudocode :
if (nodePtr == nullptr)
{
// ++ from end(). get the root of the tree
nodePtr = tree->root;
// error! ++ requested for an empty tree
if (nodePtr == nullptr)
throw UnderflowException { };
// move to the smallest value in the tree,
// which is the first node inorder
while (nodePtr->left != nullptr) {
nodePtr = nodePtr->left;
}
}
else
if (nodePtr->right != nullptr)
{
// successor is the farthest left node of
// right subtree
nodePtr = nodePtr->right;
while (nodePtr->left != nullptr) {
nodePtr = nodePtr->left;
}
}
else
{
// have already processed the left subtree, and
// there is no right subtree. move up the tree,
// looking for a parent for which nodePtr is a left child,
// stopping if the parent becomes NULL. a non-NULL parent
// is the successor. if parent is NULL, the original node
// was the last node inorder, and its successor
// is the end of the list
p = nodePtr->parent;
while (p != nullptr && nodePtr == p->right)
{
nodePtr = p;
p = p->parent;
}
// if we were previously at the right-most node in
// the tree, nodePtr = nullptr, and the iterator specifies
// the end of the list
nodePtr = p;
}
return *this;rotation :
- rotations are structural operations that ensure that the red-black tree is balanced
- there are 2 types of rotations explanation :
LEFT_ROTATION(x)y = x.right x.right = y.leftThe left child of y is going to be the right child of x -x.right = y.left. We also need to change the parent ofy.leftto x. We will do this if the left child of y is notNULL.if y.left != NULLy.left.parent = xThen we need to put y to the position of x. We will first change the parent of y to the parent of x -y.parent = x.parent. After this, we will make the node x the child of y's parent instead of y. We will do so by checking if y is the right or left child of its parent. We will also check if y is the root of the tree.y.parent = x.parentif x.parent == NULL //x is rootT.root = yelseif x == x.parent.left // x is left childx.parent.left = yelse // x is right childx.parent.right = yAt last, we need to make x the left child of y.y.left = xx.parent = yLEFT_ROTATE( x) y = x.right x.right = y.left if y.left != NULL y.left.parent = x y.parent = x.parent if x.parent == NULL //x is root T.root = y elseif x == x.parent.left // x is left child x.parent.left = y else // x is right child x.parent.right = y y.left = x x.parent = y - left rotation :
- right rotation :
- this applies to the right rotation, with just working with the opposite children
color-flipping :
- color-flipping is a red-black tree operation that ensures that the color rules of the red-black tree are not violated
- in general, when we color-flip, we make the child black and the parent red
insertion :
insertion strategy: 1 - insert the node and color it RED 2 - check what is the violation 3 - repair the violation either by color flipping, rotating, or both
1 - first of all we insert the node in the binary search way, and we color it RED, the reason for that is to not violate rule 5
2- after that if there are 2 consecutive RED nodes, we will fix it depending on the 2 cases that may occur:
- if the uncle’s node is red → we just color flip the parent red and the children red and we check for any violation for the grandparent and grand grand parent till the root
- if the uncle’s node is black → we need to rotate depending on the position of the node and the parent
INSERT(T, n)
y = T.NIL
temp = T.root
while temp != T.NIL
y = temp
if n.data < temp.data
temp = temp.left
else
temp = temp.right
n.parent = y
if y==T.NIL
T.root = n
else if n.data < y.data
y.left = n
else
y.right = n
n.left = T.NIL
n.right = T.NIL
n.color = RED
INSERT_FIXUP(T, n)cases:
- if the uncle is red, we color flip color flipping is simple :make the parent red and the children black <aside> 💡 if the parent is the root, then we force the color to be black </aside>
- if the uncle is black, we rotate : there are 4 cases when we are rotating the same applies if the parent is the grandparent’s right but in reverse
- if the parent is the grandparent left and the child is the parent’s left
- then we right rotate
- if the parent is the grandparent’s left and the child is the parent’s right
- then we need to left-right rotate
INSERT_FIXUP(T, z)
while z.parent.color == red
if z.parent == z.parent.parent.left //z.parent is left child
y = z.parent.parent.right //uncle of z
if y.color == red //case 1
z.parent.color = black
y.color = black
z.parent.parent.color = red
z = z.parent.parent
else //case 2 or 3
if z == z.parent.right //case 2
z = z.parent //marked z.parent as new z
LEFT_ROTATE(T, z) //rotated parent of orignal z
//case 3
z.parent.color = black // made parent black
z.parent.parent.color = red // made grandparent red
RIGHT_ROTATE(T, z.parent.parent) // right rotation on grandparent
else // z.parent is right child
code will be symmetric
T.root.color = blackDeletion :
The deletion process in a red-black tree is also similar to the deletion process of a normal binary search tree. Similar to the insertion process, we will make a separate function to fix any violations of the properties of the red-black tree.
Just go through the DELETE function of binary search trees because we are going to develop the code for deletion on the basis of that only. After that, we will develop the code to fix the violations.
On the basis of the TRANSPLANT function of a normal binary search tree, we can develop the code for the transplant process for red-black trees as:
RB-TRANSPLANT(T, u, v)
if u.parent == T.NIL // u is root
T.root = v
elseif u == u.parent.left //u is left child
u.parent.left = v
else // u is right child
u.parent.right = v
v.parent = u.parent
this is the pseudocode for the deletion of the binary search tree:
RB-DELETE(T, z)
y = z
y_orignal_color = y.color
if z.left == T.NIL //no children or only right
x = z.right
RB-TRANSPLANT(T, z, z.right)
else if z.right == T.NIL // only left child
x = z.left
RB-TRANSPLANT(T, z, z.left)
else // both children
y = MINIMUM(z.right)
y_orignal_color = y.color
x = y.right
if y.parent == z // y is direct child of z
x.parent = y
else
RB-TRANSPLANT(T, y, y.right)
y.right = z.right
y.right.parent = y
RB-TRANSPLANT(T, z, y)
y.left = z.left
y.left.parent = y
y.color = z.color
if y_orignal_color == black
RB-DELETE-FIXUP(T, x)Fixing Violation of Red-Black Tree in Deletion
The violation of property 5 (change in black height) is our main concern. We are going to deal it with in a bit different way. We are going to say that property 5 has not been violated and node x which is now occupying y 's original position has an "extra black" in it. In this way, the property of black height is not violated but the property 1 i.e., every node should be either black or red is violated because the node x is now either "double black" (if it was black) or red and black (if it was red).
With this thinking, we can say that either properties 1, 2, or 4 can be violated.
If x is red and black, we can simply color it black and this will fix the violation of property 1 without causing any other violation. This will also solve the violation of property 4.
If x is the root, we can simply remove the one extra black and thus, fix the violation of property 1.
With the above two mentioned cases, violations of properties 2 and 4 are entirely solved and now we can focus only on solving the violation of property 1.
There can be 4 cases (w is x's sibling):
- w is red.
- w is black and both children are black.
- w is black and its right child is black and the left child is red.
- w is black and its right child is red.
let’s explain each case for fixup on its own

- w is
red: we can switch the colors of w and its parent and then left-rotate the parent of x In this way, we will enter either case 2, 3, or 4.

- w is black and both its children are black : For the second case, we will take out one black from both x and w This will leave x black (it was double black) and y red. For the compensation, we will put one extra black on the parent of x and mark it as the new x and repeat the entire process of fixing the violations for the new x Screen Shot 2022-11-27 at 12.53.21 PM.png
Take note that by doing this, we haven't caused any new violations.
Also if we have entered case 2 from case 1, the parent must be red and now red and black, so it will be simply fixed by coloring it black.

- We will transform case 3 to case 4 by switching the colors of w and its left child and then rotating right w

- For case 4, take a look at the following picture: Screen Shot 2022-11-27 at 12.58.24 PM.png
We first colored w the same as the parent of x and then colored the parent of x black. After this, we colored the right child of w black and then left rotated the parent of x. At last, we removed the extra black from x without violating any properties.
pseudo-code for the fixup delete :
RB-DELETE-FIXUP(T, x)
while x!= T.root and x.color == black
if x == x.parent.left
w = x.parent.right
if w.color == red //case 1
w.color = black
x.parent.color = red
LEFT-ROTATE(T, x.parent)
w = x.parent.right
if w.left.color == black and w.right.color == black //case 2
w.color = red
x = x.parent
else //case 3 or 4
if w.right.color == black //case 3
w.left.color = black
w.color = red
RIGHT-ROTATE(T, w)
w = x.parent.right
//case 4
w.color = x.parent.color
x.parent.color = black
w.right.color = black
LEFT-ROTATE(T, x.parent)
x = T.root
else
code will be symmetric
x.color = black5 - FT_MAP
now that we have a working red-black tree, map is almost free. a std::map is an ordered associative container: keys are unique, kept sorted by Compare, and each key maps to a value. under the hood that is exactly a balanced BST keyed on the .first of each pair.
the only real trick is the comparator. the tree compares whole value_types, but map only cares about the key. so we wrap the user's comparator into a value_compare that reaches into .first:
template <class Key, class T, class Compare = std::less<Key> >
class map {
public:
typedef ft::pair<const Key, T> value_type;
// adapt: order two value_types by their keys only
class value_compare {
Compare _comp;
public:
value_compare(Compare c) : _comp(c) {}
bool operator()(const value_type& a, const value_type& b) const {
return _comp(a.first, b.first);
}
};
private:
typedef ft::RBTree<value_type, value_compare> tree_type;
tree_type _tree;
};operator[]
the famous one. m[key] inserts a default-constructed value if the key is missing, then hands back a reference to it:
T& operator[](const Key& key) {
return _tree.insert(ft::make_pair(key, T())).first->second;
}notice it can't be const — it may insert. that is why there is no const overload (in c++11 you reach for .at() instead, which throws rather than inserts).
iterators come for free
because the tree already exposes in-order iterators, map::begin()/end() just forward to the tree. in-order traversal of a BST keyed on .first visits pairs sorted by key — which is precisely map's contract. you wrote the hard part once, in the tree, and every ordered container inherits it.
6 - FT_SET
set is map with the value taken away — the key is the value. same red-black tree, but value_type == Key and the comparator orders keys directly.
one rule from the standard catches people out: set iterators are always const. you can't mutate an element in place, because changing it would silently break the tree's ordering invariant. so iterator and const_iterator are the same type — mutation has to go through erase + insert.
template <class T, class Compare = std::less<T> >
class set {
typedef ft::RBTree<const T, Compare> tree_type;
// iterator == const_iterator, by design
};7 - reverse_iterator
every container also owes you rbegin() and rend(). instead of writing reversed traversal by hand, the STL gives you an adapter: reverse_iterator wraps any bidirectional iterator and flips its direction.
the subtle bit is the off-by-one. a reverse iterator stores a base iterator and dereferences the element before it. that single indirection is what lets rbegin() (base = end()) point at the last real element, and rend() (base = begin()) sit one before the first:
template <class Iter>
class reverse_iterator {
Iter _base;
public:
explicit reverse_iterator(Iter it) : _base(it) {}
reference operator*() const { Iter tmp = _base; return *--tmp; }
reverse_iterator& operator++() { --_base; return *this; }
reverse_iterator& operator--() { ++_base; return *this; }
Iter base() const { return _base; }
};write the adapter once and every container gets reverse iteration for free — the same leverage as iterator traits and allocators. that repetition is the whole point: the STL is small because each piece does one job and composes with the rest.
what reimplementing the STL actually teaches you
the headline lesson is that there is no magic. vector is a pointer, a size, and a capacity, with a careful dance around std::allocator and exception safety. map is a red-black tree wearing a comparator. iterators are just a typed cursor with five typedefs that let algorithms ask "what are you?" at compile time. once you've built them, you stop treating the standard library as a black box and start seeing the moving parts.
the second lesson is harder won: the difficulty was never the data structures — it was the contracts. strong exception safety on insert. iterator invalidation rules. const-correctness that the type system enforces for you. the rebalancing case analysis in red-black deletion. these are the invisible promises every STL container keeps, and you only really feel their weight when you're the one who has to keep them.
and that is exactly why the exercise is worth it. you come out reading other people's code differently — you see the allocation behind the convenience, the template substitution behind the overload, the syscall behind the abstraction. the same way writing an HTTP server from the socket up taught me what every framework quietly does on my behalf, rebuilding the STL taught me what every std:: quietly does on mine.
build the boring machinery yourself, once. then trust it — because now you know exactly what it's doing.