Content
STL Basics/a> Containers Functors and Lambdas Using the STL STL Container Performance <- Go BackUniversity of Michigan at Ann Arbor
Last Edit Date: 06/04/2023
Disclaimer and Term of Use:
We do not guarantee the accuracy and completeness of the summary content. Some of the course material may not be included, and some of the content in the summary may not be correct. You should use this file properly and legally. We are not responsible for any results from using this file
This personal note is adapted from Professor Marcus Darden and David Paoletti. Please contact us to delete this file if you think your rights have been violated.
This work is licensed under a Creative Commons Attribution 4.0 International License.
i. Introduction
STL stands for Standard Template Library
Included in C++, expanded in C++ 11
Part of stdlibc++ (not stdlibc)
Well-documented
High-quality implementations of best algorithms and data structs at your fingerprints
All implementations are entirely in headers
No linking necessary
All code are available
ii. Contents of STL
Containers and iterators
Memory allocators
Utilities and function objects
Algorithms
iii. STL Resources
The C++ Language, 4e by Bjarne Stroustrup
The C++ Standard Library: A Tutorial and Reference, 2e by Nicolai Josuttis (covers C++ 11)
See cppreference.com ("run this code" feature)
See cplusplus.com ("edit & run" feature)
iv. Using Libraries v.s. Do-it-Yourself
Pros:
Some algorithms and data structures are hard to implement
Some are hard to implement well
stable_sort()
)Uniformly for simple algorithms
max<>()
, swap<>()
, set_union<>()
Reduce debugging time for complicated programs
$>$ 50% development time is spent test & debugging
Using high-quality libraries reduces debugging time
Cons:
Libraries only contain general-purpose implementations
Specializad code may run faster
Trade-offs
Need to understand a library well to fully utillize it
Data structures
Algorithms
Complexities of operations
Example:
STL sort()
Implemented with $O(n \log n)$ worst case time
In practice is typically faster than quick sort
STL nth_element()
In older STL, linked lists did not store their size
v. C++ Features that STL Relies On
Type bool
const-correctness and const-casts
Namespaces: using namespace std;
, using std::vector
;
Templates
Inline functions
Exception handling
Implicit initialization
Operator overloading
Extended syntax for new()
Keywords explicit
and mutable
explicit
should be used with 1-parameter constructors to prevent accidental conversion from another type.explicit FeetInches(int feet);
FeetInches a(3); // OK: 3 feet, 0 inches
FeetInches b = 3; // Error
mutable
member variable can be modified by a const
member functionvi. Pointers, Generic Programming
STL helps minimize use of pointers and dynamic allocation
Can reuse same algorithms with multiple data structures
vii. Performance and Big-O
Most STL implementations have the best possible big-O complexities, given their interface
Some have poor performance even with a good implementation (linked list)
Note: Main priority in STL is time performancel it is very difficult to beat the STL's speed
i. STL Containers
Basic Containers
vector<>
and deque<>
stack<>
and queue<>
are "adaptors"bit_vector
is same as vector<bool>
set<>
and multi_set<>
(plus unordered)
map<>
and multi_map<>
(plus unordered)
list<>
array<>
(limited use, size fixed at compile time)
Linked List Containers
Container | Pointers | .size() |
---|---|---|
list<> |
Doubly-linked | $O(1)$ |
slist<> |
Singly-linked | Can be $O(n)$ |
forward_list<> |
Singly-linked | Does not exist |
Note: slist<>
includes smart pointers
ii. Using Iterators
Iterators generalize pointers
Allow for implementation of the same algorithm for multiple data structure
Support the concept of sequential containers
Iterators help writing faster code for traversals
ar[i++]
to *(it++)
Types of Iterators
Access members of a container class
Similar to pointers; all can be copy-constructed
input_iterator |
Read values with forward movement. No multiple passes. Can be incremented, compared, and dereferernced. |
output_iterator |
Write values with forward movement. No multiple passes. Can be incremented and dereferernced. |
forward_iterator |
Read values with forward movement. No multiple passes. Can be incremented, compared, dereferernced, and store the iterator's value. Can access the same value more than once. |
bidirectional_iterator |
Same as forward_iterator but can also decrement. |
random_iterator |
Same as bidirectional_iterator but can also do pointer arithmetic and pointer caomparisons. |
reverse_iterator |
An iterator adaptor (that inherits from either a random_iterator or a bidirectional_iterator ) whose ++ operations moves in reverse. |
Iterator Ranges
All STL containers that support iterators support
.begin()
, .end()
, .cbegin()
, .cend()
, .rbegin()
, .rend()
"begin" is inclusive, "end" is exclusive (one past last)
STL operates on iterators ranges, not containers
A range can capture any fraction of a container
Iterator ranges (unlike indices) need no random access
Faster traversal than with indices
Copying and Sorting
In C++ 11+:
1 #include <vector>
2 #include <algorithm>
3 using namespace std;
4 const size_t N = 100;
5
6 int main() {
7 vector<int> v(N, -1);
8 int a[N];
9
10 for (size_t j = 0; j != N; ++j)
11 v[j] = (j * j * j) % N;
12 copy(v.begin(), v.end(), a); // copy over
13 copy(a, a + N, v.begin()); // copy back
14 sort(a, a + N);
15 sort(v.begin(), v.end());
16 vector<int> reversed(v.rbegin(), v.rend());
17 } // main()
In C++ 14+:
1 #include <vector>
2 #include <algorithm>
3 using namespace std;
4 const size_t N = 100;
5
6 int main() {
7 vector<int> v(N, -1);
8 int a[N];
9
10 for (size_t j = 0; j != N; ++j)
11 v[j] = (j * j * j) % N;
12 copy(begin(v), end(v), begin(a)); // copy over
13 copy(begin(a), end(a), begin(v)); // copy back
14 sort(begin(a), end(a));
15 sort(begin(v), end(v));
16 vector<int> reversed(rbegin(v), rend(v));
17 } // main()
iii. Not Using Iterators
Sometimes implementing multiple versions might generate ambiguities.
A better method is as following:
// Overload for each container type you need to output
template <class T>
ostream &operator<<(ostream &out, const vector<T> &c) {
for (auto &x : c)
out << x << " ";
return out;
}
This code compiles without ambiguities
Needs multiple versions for list<>
, deque<>
, etc.
iv. Memory Allocation & Initialization
Initializing elements of a container
Container of pointers
Behind-the-scenes memory allocation
Data structure | Memory overhead |
---|---|
vector<> |
Compact |
list<> |
Not very compact |
unordered_map<> |
Memory hog |
std::vector
Memory Overhead
Three pointers (3 * 8 bytes) - $O(1)$ space
Begin allocated memory
End allocated memory
End used memory
vector<SmallClass>
vs. vector<LargeVlass>
Large overhead when using many small vectors
vector<vector<vector<T>>> ar3d(a, b, c);
Reorder dimensions to reduce overhead: a < b < c
i. Functors
Suppose a class Employee needs to be sorted
Do not overload operator<()
Use helper class: a functional object or "functor"
struct SortByName {
bool operator()(const Employee &left,
const Employee &right) {
return left.getName() < right.getName();
}
};
Filling a Container with Values
Instead of using a loop, there is a simple function called iota
, standard as of C++ 11.
// Fill a vector with values, starting at 0
#include <numeric>
vector<int> v(100);
iota(begin(v), end(v), 0);
// v contains {0, 1, 2, 3, ..., 99}
i. Lambdas
A lambda is an anonymous function object that can be defined on the fly, directly in the code where it is called.
Improves code readability by keeping code localized (no need to name a function elsewhere that you will only ever use once)
Makes STL algorithms easier to use
Can be passed around and used as function arguments (ex: callback functions)
Anatomy of a Lambda
A lambda expression consists of the following:
[captures list] (parameter list) {function body}
The captures list specifies variables from the current scope that will be available inside the lambda
The parameter list specifies the argument names and types passed into the lambda
The function body defines the behavior of the lambda
The following lambda takes in two integers and checks if their difference is greater than 5:
[] (int n1, int n2) { return abs(n1 - n2) > 5; }
Compiler will attempt to deduce the return type of a lambda function body whenever possible
The arrow operator can be used to explicitly specify a return type
When type returned is different from args
When type returned cannot be implicitly deduced
[captures list] (parameter list) -> return_type {function body}
[] (double x, double y) -> int { return x + y; }
Example:
1 struct IsOddPred { // Returns true if number is odd
2 bool operator()(int n) {
3 return n % 2 == 1;
4 } // operator()()
5 }; // IsOddPred{}
6
7 int main() {
8 vector<int> vec = {24, 32, 54, 86, 53, 47, 92, 61};
9
10 // Using the functor defined above
11 auto it1 = find_if(begin(vec), end(vec), IsOddPred());
12
13 // Using a lambda defined in place
14 auto it2 = find_if(begin(vec), end(vec), [](int n) {
15 return n % 2 == 1;
16 }); // find_if()
17 } // main()
Try it out:
Lambda Variable Capture
A lambda can use parameters and other variables in its function body
To use variables from its surrounding scope, captures are required
Variables placed within []
before the parameter list are captured (by value or reference with &
)
1 int main() {
2 vector<int> vec = {45, 63, 28, 21, 80, 91, 88, 72, 34, 57, 50, 69};
3 int factor;
4
5 cout << "Enter a number between 1 and 25: " << endl;
6 cin >> factor;
7 cout << "The first number that is a multiple of " << factor << " is: ";
8 cout << *find_if(begin(vec), end(vec), [factor](int n) {
9 return n % factor == 0;
10 }); // find_if()
11 } // main()
There are several ways to capture variables in a lambda:
[]
captures no variables fromt he surrounding scope
[=]
captures all varibales in the surrounding scope by value (i.e. a copy of each variable is made)
[&]
captures all variables in the surrounding scope by reference
[foo]
captures only the variable foo
, by value
[&foo]
captures only the variable foo
, by reference
[foo, bar]
captures only the variables foo
and bar
, by value
[=, &foo]
captures all variables in the surrounding scope by value except for foo
, which is captured by reference
[&, foo]
captures all variables in the surrounding scope by reference except for foo
, which is captured by value
[this]
captures the current object - needed if a lambda defined in an object's member function needs access to is member variables
i. Learning STL
Online tutorials and examples
Discusses possible implementations of STL functions
You can copy / modify examples and run them online
Practice with small tester programs
Detailed coverage in books
Ways to learn a library
Skim through documentation
Study what seems interesting
Learn how to use those pieces
Come back when you need something
ii. Debugging STL-heavy code
Compiler Errors
Compiler often complains about STL headers, not your code - induced errors
You will need to shift through many lines of messages, to find line reference to your code
Good understanding of type conversions in C++ is often required to fix problems
Double-check function signatures
Runtime Debugging
Crashes can occur in STL code, started by an error in your code
Debugging needed with ANY library
Ing gdb, use "where" or "bt" commands to find code that calls STL
90% of STL-related crashes are due to user's dangling pointers or references going out og scope
iii. Randomization
C++ 11 introduced the <random>
library to support random number generation:
Generators: generate random numbers
std::random_device
, std::mt19937
, std::mt19937_64
Distributions: convert generator output into staistical distributions
std::normal_distribution
, std::uniform_int_distribution
, std::uniform_real_distribution
Generally preferred over rand()
Randomization is great for testing code
Example 1:
1 #include <iostream>
2 #include <vector>
3 #include <algorithm> // Needed for shuffle()
4 #include <numeric> // Needed for iota()
5 #include <random> // Needed for random_device and mt19937_64
6 using namespace std;
7
8 int main() {
9 random_device rd; // Create a device to start random # generation
10 mt19937_64 mt(rd()); // Create a Mersenne Twister to generate random #
11 int size = 20; // Could also read size from cin
12 vector<int> values(size);
13
14 iota(begin(values), end(values), 0);
15 shuffle(begin(values), end(values), mt);
16
17 for (auto v : values)
18 cout << v << " ";
19
20 cout << endl;
21 return 0;
22 }
Example 2:
#include <iostream>
#include <random> // Needed for random_device and mt19937_64
using namespace std;
int main() {
random_device rd; // Create a device to start random # generation
mt19937_64 mt(rd()); // Create a Mersenne Twister to generate random #
int size = 10; // Could also read size from cin
cout << size << "\n";
// Run a loop, generating random x / y coordinates (important for project 4)
for (int i = 0; i < size; i++) {
// mt() generates a large random number, % limits the range, - makes
// about half the numbers negative; the range is -10 to 10 (inclusive)
int x = static_cast<int>(mt() % 21) - 10;
int y = static_case<int>(mt() % 21) - 10;
cout << x << ' ' << y << '\n';
}
cout << endl;
return 0;
}
Filling an empty vector with different values. (vector_pre used vector::resize()
, which is an single allocation)
Fill the container with numbers $[0, N)$ shuffle at random; search for each value using std::find()
.
Fill the container with numbers $[0, N)$ shuffle at random; pick a random position by linear search; insert 1000 values.
Fill the container with numbers $[0, N)$ shuffle at random; pick a random position by linear search; remove 1000 values.
Insert new values at the front. A vector needs to move all prior elements, but a list does not.