The Standard Template Library (STL) is a powerful feature of C++ that provides a set of common data structures and algorithms. It’s a must-know for any C++ programmer who wants to write efficient and maintainable code. In this tutorial, we will dive deep into the STL, covering its various components and showing you how to leverage them effectively in your programs.
1. Introduction to STL
What is STL?
The Standard Template Library (STL) is a set of C++ template classes to provide common programming data structures and functions such as lists, stacks, arrays, etc. It is a library of container classes, algorithms, and iterators.
Advantages of Using STL
- Reusability: STL provides ready-to-use implementations of common data structures and algorithms.
- Efficiency: STL algorithms are typically optimized for performance.
- Consistency: Using STL ensures that your code is consistent and follows the same conventions and practices as other C++ code.
- Generic Programming: STL uses templates, which means you can use the same code with different data types without rewriting it.
2. Containers
Containers are the foundation of the STL. They store collections of objects. Containers can be broadly categorized into three types: Sequence Containers, Associative Containers, and Unordered Containers.
Sequence Containers
Vector
std::vector
is a dynamic array that can change size automatically when elements are added or removed. It provides fast random access and is efficient for insertions and deletions at the end.
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
v.push_back(6); // Adds 6 at the end
v.pop_back(); // Removes last element
for (int i : v) {
std::cout << i << " ";
}
return 0;
}
Code language: C++ (cpp)
List
std::list
is a doubly linked list. It allows fast insertions and deletions from anywhere in the sequence.
#include <list>
#include <iostream>
int main() {
std::list<int> l = {1, 2, 3, 4, 5};
l.push_back(6); // Adds 6 at the end
l.push_front(0); // Adds 0 at the beginning
for (int i : l) {
std::cout << i << " ";
}
return 0;
}
Code language: C++ (cpp)
Deque
std::deque
is a double-ended queue that allows fast insertions and deletions at both the beginning and the end.
#include <deque>
#include <iostream>
int main() {
std::deque<int> d = {1, 2, 3, 4, 5};
d.push_back(6); // Adds 6 at the end
d.push_front(0); // Adds 0 at the beginning
for (int i : d) {
std::cout << i << " ";
}
return 0;
}
Code language: C++ (cpp)
Associative Containers
Set
std::set
is a collection of unique keys, sorted by the keys themselves.
#include <set>
#include <iostream>
int main() {
std::set<int> s = {1, 2, 3, 4, 5};
s.insert(6); // Inserts 6
s.erase(3); // Removes 3
for (int i : s) {
std::cout << i << " ";
}
return 0;
}
Code language: C++ (cpp)
Map
std::map
is a collection of key-value pairs, sorted by keys.
#include <map>
#include <iostream>
int main() {
std::map<int, std::string> m;
m[1] = "one";
m[2] = "two";
m[3] = "three";
for (const auto &p : m) {
std::cout << p.first << " => " << p.second << "\n";
}
return 0;
}
Code language: C++ (cpp)
Multiset and Multimap
std::multiset
and std::multimap
allow duplicate keys.
#include <set>
#include <map>
#include <iostream>
int main() {
std::multiset<int> ms = {1, 2, 2, 3, 3, 3};
std::multimap<int, std::string> mm;
mm.insert({1, "one"});
mm.insert({2, "two"});
mm.insert({2, "dos"});
for (int i : ms) {
std::cout << i << " ";
}
std::cout << "\n";
for (const auto &p : mm) {
std::cout << p.first << " => " << p.second << "\n";
}
return 0;
}
Code language: C++ (cpp)
Unordered Containers
Unordered containers use hashing for faster access compared to ordered containers.
Unordered Set
std::unordered_set
is a collection of unique keys with no particular order.
#include <unordered_set>
#include <iostream>
int main() {
std::unordered_set<int> us = {1, 2, 3, 4, 5};
us.insert(6);
us.erase(3);
for (int i : us) {
std::cout << i << " ";
}
return 0;
}
Code language: C++ (cpp)
Unordered Map
std::unordered_map
is a collection of key-value pairs with no particular order.
#include <unordered_map>
#include <iostream>
int main() {
std::unordered_map<int, std::string> um;
um[1] = "one";
um[2] = "two";
um[3] = "three";
for (const auto &p : um) {
std::cout << p.first << " => " << p.second << "\n";
}
return 0;
}
Code language: C++ (cpp)
Unordered Multiset and Unordered Multimap
std::unordered_multiset
and std::unordered_multimap
allow duplicate keys with no particular order.
#include <unordered_set>
#include <unordered_map>
#include <iostream>
int main() {
std::unordered_multiset<int> ums = {1, 2, 2, 3, 3, 3};
std::unordered_multimap<int, std::string> umm;
umm.insert({1, "one"});
umm.insert({2, "two"});
umm.insert({2, "dos"});
for (int i : ums) {
std::cout << i << " ";
}
std::cout << "\n";
for (const auto &p : umm) {
std::cout << p.first << " => " << p.second << "\n";
}
return 0;
}
Code language: C++ (cpp)
Container Adapters
Container adapters provide a different interface for sequential containers.
Stack
std::stack
is a container adapter that gives the functionality of a stack (LIFO).
#include <stack>
#include <iostream>
int main() {
std::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty()) {
std::cout << st.top() << " ";
st.pop();
}
return 0;
}
Code language: C++ (cpp)
Queue
std::queue
is a container adapter that gives the functionality of a queue (FIFO).
#include <queue>
#include <iostream>
int main() {
std::queue<int> q;
q.push(1);
q.push(2);
q.push(3);
while (!q.empty()) {
std::cout << q.front() << " ";
q.pop();
}
return 0;
}
Code language: C++ (cpp)
Priority Queue
std::priority_queue
is a container adapter that provides the functionality of a heap.
#include <queue>
#include <iostream>
int main() {
std::priority_queue<int> pq;
pq.push(3);
pq.push(1);
pq.push(2);
while (!pq.empty
()) {
std::cout << pq.top() << " ";
pq.pop();
}
return 0;
}
Code language: C++ (cpp)
3. Iterators
Iterators are used to point at the memory addresses of STL containers. They are primarily used in sequence of numbers, characters, etc.
Input Iterators
Input iterators can read from the container.
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int>::iterator it;
for (it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " ";
}
return 0;
}
Code language: C++ (cpp)
Output Iterators
Output iterators can write to the container.
#include <vector>
#include <iostream>
#include <iterator>
int main() {
std::vector<int> v(5);
std::vector<int>::iterator it;
int value = 1;
for (it = v.begin(); it != v.end(); ++it) {
*it = value++;
}
for (int i : v) {
std::cout << i << " ";
}
return 0;
}
Code language: C++ (cpp)
Forward Iterators
Forward iterators can read/write from/to the container in a forward direction.
#include <list>
#include <iostream>
int main() {
std::list<int> l = {1, 2, 3, 4, 5};
std::list<int>::iterator it;
for (it = l.begin(); it != l.end(); ++it) {
std::cout << *it << " ";
}
return 0;
}
Code language: C++ (cpp)
Bidirectional Iterators
Bidirectional iterators can move in both directions.
#include <list>
#include <iostream>
int main() {
std::list<int> l = {1, 2, 3, 4, 5};
std::list<int>::reverse_iterator it;
for (it = l.rbegin(); it != l.rend(); ++it) {
std::cout << *it << " ";
}
return 0;
}
Code language: C++ (cpp)
Random Access Iterators
Random access iterators can move anywhere in the container.
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int>::iterator it;
it = v.begin();
std::cout << *(it + 2) << "\n"; // Random access
return 0;
}
Code language: C++ (cpp)
4. Algorithms
The STL provides many algorithms that work on iterators to perform various operations.
Non-modifying Algorithms
These algorithms do not modify the container.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
// Find an element
auto it = std::find(v.begin(), v.end(), 3);
if (it != v.end()) {
std::cout << "Found: " << *it << "\n";
}
// Count elements
int count = std::count(v.begin(), v.end(), 3);
std::cout << "Count: " << count << "\n";
return 0;
}
Code language: C++ (cpp)
Modifying Algorithms
These algorithms modify the container.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
// Fill
std::fill(v.begin(), v.end(), 0);
for (int i : v) {
std::cout << i << " ";
}
std::cout << "\n";
// Replace
std::replace(v.begin(), v.end(), 0, 1);
for (int i : v) {
std::cout << i << " ";
}
std::cout << "\n";
return 0;
}
Code language: C++ (cpp)
Sorting Algorithms
These algorithms sort the container.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {5, 3, 1, 4, 2};
// Sort
std::sort(v.begin(), v.end());
for (int i : v) {
std::cout << i << " ";
}
std::cout << "\n";
// Reverse
std::reverse(v.begin(), v.end());
for (int i : v) {
std::cout << i << " ";
}
std::cout << "\n";
return 0;
}
Code language: C++ (cpp)
Numeric Algorithms
These algorithms perform numeric operations on the container.
#include <numeric>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
// Accumulate
int sum = std::accumulate(v.begin(), v.end(), 0);
std::cout << "Sum: " << sum << "\n";
// Inner product
int product = std::inner_product(v.begin(), v.end(), v.begin(), 1);
std::cout << "Product: " << product << "\n";
return 0;
}
Code language: C++ (cpp)
5. Functional
STL also provides functional utilities that are used to manipulate functions and function objects.
Function Objects
Function objects or functors are objects that can be used as functions.
#include <iostream>
#include <vector>
#include <algorithm>
struct MultiplyBy {
int factor;
MultiplyBy(int f) : factor(f) {}
int operator()(int x) const { return x * factor; }
};
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
std::transform(v.begin(), v.end(), v.begin(), MultiplyBy(2));
for (int i : v) {
std::cout << i << " ";
}
return 0;
}
Code language: C++ (cpp)
Bind and Bind2nd
std::bind
and std::bind2nd
are used to bind arguments to functions or functors.
#include <iostream>
#include <functional>
int add(int a, int b) {
return a + b;
}
int main() {
auto add_five = std::bind(add, 5, std::placeholders::_1);
std::cout << add_five(3) << "\n"; // Outputs 8
return 0;
}
Code language: C++ (cpp)
Function and Mem_fn
std::function
and std::mem_fn
are used to store and call any callable object.
#include <iostream>
#include <functional>
struct Foo {
void print_sum(int a, int b) {
std::cout << a + b << "\n";
}
};
int main() {
Foo foo;
auto fn = std::mem_fn(&Foo::print_sum);
fn(foo, 3, 5);
return 0;
}
Code language: C++ (cpp)
Lambda Expressions
Lambda expressions are a convenient way to create anonymous function objects.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
std::for_each(v.begin(), v.end(), [](int &x) { x *= 2; });
for (int i : v) {
std::cout << i << " ";
}
return 0;
}
Code language: C++ (cpp)
6. Utilities
STL also includes several utility functions and classes that simplify common programming tasks.
Pair and Tuple
std::pair
and std::tuple
are used to store fixed-size collections of heterogeneous values.
#include <iostream>
#include <utility>
#include <tuple>
int main() {
std::pair<int, std::string> p = std::make_pair(1, "one");
std::cout << p.first << " => " << p.second << "\n";
std::tuple<int, std::string, double> t = std::make_tuple(1, "one", 3.14);
std::cout << std::get<0>(t) << " => " << std::get<1>(t) << " => " << std::get<2>(t) << "\n";
return 0;
}
Code language: C++ (cpp)
Move Semantics
Move semantics allows the resources owned by an rvalue to be moved rather than copied.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = std::move(v1); // v1 is now empty
for (int i : v2) {
std::cout << i << " ";
}
std::cout << "\n";
std::cout << "v1 size: " << v1.size() << "\n"; // v1 size: 0
return 0;
}
Code language: C++ (cpp)
Forwarding
Forwarding allows you to pass arguments while preserving their value category (lvalue or rvalue).
#include <iostream>
#include <utility>
void print(int &x
) {
std::cout << "lvalue: " << x << "\n";
}
void print(int &&x) {
std::cout << "rvalue: " << x << "\n";
}
template <typename T>
void forward_print(T &&x) {
print(std::forward<T>(x));
}
int main() {
int a = 10;
forward_print(a); // Calls lvalue version
forward_print(20); // Calls rvalue version
return 0;
}
Code language: C++ (cpp)
7. Practical Examples
Implementing Common Algorithms
Let’s implement a few common algorithms using STL.
Finding the Maximum Element
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v = {1, 3, 5, 7, 2, 4, 6};
auto max_elem = std::max_element(v.begin(), v.end());
std::cout << "Max element: " << *max_elem << "\n";
return 0;
}
Code language: C++ (cpp)
Removing Duplicates from a Vector
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v = {1, 2, 2, 3, 4, 4, 5};
std::sort(v.begin(), v.end());
auto last = std::unique(v.begin(), v.end());
v.erase(last, v.end());
for (int i : v) {
std::cout << i << " ";
}
return 0;
}
Code language: C++ (cpp)
Finding the Intersection of Two Sets
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = {3, 4, 5, 6, 7};
std::vector<int> v3;
std::sort(v1.begin(), v1.end());
std::sort(v2.begin(), v2.end());
std::set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), std::back_inserter(v3));
for (int i : v3) {
std::cout << i << " ";
}
return 0;
}
Code language: C++ (cpp)
Performance Considerations
When using STL, it’s important to choose the right container and algorithm to achieve optimal performance.
- Vectors vs. Lists: Use
std::vector
for most cases as it provides better cache performance. Usestd::list
only when frequent insertions and deletions from the middle of the container are required. - Maps vs. Unordered Maps: Use
std::map
when you need ordered keys. Usestd::unordered_map
for faster access when the order is not important.
Best Practices
- Use Algorithms: Prefer using STL algorithms over writing your own loops.
- Prefer Const Iterators: Use const iterators when you do not need to modify the elements.
- Use Auto: Use
auto
for iterator declarations to improve readability. - Leverage Emplace Functions: Use
emplace
overinsert
for in-place construction of elements.
Conclusion
The Standard Template Library (STL) is a powerful toolset in C++ that provides ready-to-use data structures and algorithms. By understanding and utilizing STL, you can write more efficient, maintainable, and clean C++ code. This tutorial has covered the basics and some advanced features of STL, equipping you with the knowledge to leverage STL in your projects. Remember, practice is key to mastering STL. Try to solve different problems using STL to become more proficient.