Module 0 ¡ What the STL Is, and Why It Changes How You Write C++
How to Read cppreference, and What O(log n) Promises
In this lesson
- Find the four things that matter on a cppreference page: the signatures, the parameters, the complexity and the iterator invalidation note.
- Read a complexity line as a promise, including what "amortised" means.
- Explain what a "since C++20" marker means for code you run on the Playground.
David is reading about std::vector. Next to push_back the reference says "constant, amortised". Next to insert it says the cost is linear in the distance to the end. He asks the obvious question: "Then why would anyone ever write insert?"
It is a good question, and the answer is on the same page, if you know where to look. This lesson teaches you to read that page. After it, every cost in this track is something you can check for yourself instead of taking on trust.
The reference every C++ programmer keeps open
Two documents describe the STL. The first is the C++ standard itself, the official ISO document that every compiler follows. It is precise and long, and the published version costs money. The committee's working drafts are free, and they are what people read: N4659 is the final draft of C++17, N4861 of C++20, and eel.is/c++draft shows the current one as web pages.
The second is cppreference.com, a free reference written by volunteers from the standard. It has one page per function, with examples, and it marks what changed in each standard. It is the page working C++ programmers keep open. Read cppreference day to day, and go to the draft when you need the exact wording.
The anatomy of a reference page
Open the page for std::vector<T>::insert. It looks dense, but it always has the same parts in the same order, and four of them carry almost everything you need.
1. The signatures. These are the declarations: every way you can call the function. insert has several, numbered (1), (2) and so on, and the rest of the page refers to them by number. Here are its C++17 forms:
iterator insert( const_iterator pos, const T& value ); (1)
iterator insert( const_iterator pos, T&& value ); (2)
iterator insert( const_iterator pos, size_type count, const T& value ); (3)
template< class InputIt >
iterator insert( const_iterator pos, InputIt first, InputIt last ); (4)
iterator insert( const_iterator pos, std::initializer_list<T> ilist ); (5)
Read them for what they take and what they return. Every form takes pos, an iterator saying where to insert, and every form returns an iterator. Form (3) inserts count copies of a value.
2. The parameters. One line per name in the signatures. Here you learn that the new elements go before pos, which is easy to guess wrong.
3. The complexity. The cost, written as a promise. For form (1) it is constant plus linear in the distance between pos and the end. In plain words, every element after the insertion point must move one place to make room.
4. Iterator invalidation. A short paragraph near the top says which old iterators still work afterwards. For insert: if the vector had to grow, every iterator into it is now invalid. Otherwise, only those at or after the insertion point are. You met this idea in Lesson 4's doubts, and Module 11 shows it moving.
So David's answer is on the page. insert exists because sometimes you need an element in the middle, and the complexity line tells you exactly what that costs.
Calling a signature you just read
auto it = v.insert(v.begin() + 1, 15);
v.insert: the function, a member of the vector.v.begin() + 1: theposparameter, an iterator. The new element goes before the element at position 1.15: thevalueparameter. This call matches form (1) or (2).auto it =: the return value, an iterator pointing at the new element.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = {10, 20, 30};
auto it = v.insert(v.begin() + 1, 15);
std::cout << "inserted " << *it << ", now:";
for (int x : v) std::cout << ' ' << x;
std::cout << '\n';
v.insert(v.end(), 3, 99);
std::cout << "three 99s at the end:";
for (int x : v) std::cout << ' ' << x;
std::cout << '\n';
}
inserted 15, now: 10 15 20 30
three 99s at the end: 10 15 20 30 99 99 99
The first call is form (1): one value, before position 1. The second is form (3): a count and a value, before end(), which is the same as "at the end". Each call is the page's signature with real arguments.
The complexity words
Reference pages describe costs in a few fixed words. Lesson 3 gave you three of them. Here is the full set you will meet in this track, each with one real operation that has it.
| The page says | Big-O | In plain words | One operation with this cost |
|---|---|---|---|
| Constant | O(1) | does not grow with n | v[i] on a vector |
| Logarithmic | O(log n) | doubling n adds about one step | s.find(x) on a set |
| Linear | O(n) | ten times n, ten times the work | std::find on a range |
| Linearithmic, or N log N | O(n log n) | a little more than linear | std::sort |
| Amortised constant | O(1) on average | constant on average over many calls | v.push_back(x) |
The last row needs a closer look, because it is the one David asked about. Amortised means "on average over many calls". Most calls to push_back are one step. Now and then the vector is full, so it moves to a bigger block of memory and copies every element. That one call is slow. Because the capacity grows by a factor each time, the slow calls are rare enough that the average stays constant.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v;
std::size_t last = v.capacity();
std::cout << "capacity changes:";
for (int i = 1; i <= 100; i++) {
v.push_back(i);
if (v.capacity() != last) {
last = v.capacity();
std::cout << ' ' << last;
}
}
std::cout << '\n';
}
capacity changes: 1 2 4 8 16 32 64 128
capacity() is how many elements fit before the vector must move. In 100 pushes it took a new block of memory only 8 times, doubling each time. The moves copied 1 + 2 + 4 + 8 + 16 + 32 + 64 = 127 elements in total, fewer than two per push. Doubling is GCC's choice; the standard only promises the average.
Checking a promise by measuring it
A complexity line is a promise about growth, not a time in seconds. So the honest test is to grow n and watch. This program builds a vector of n numbers twice. First it uses push_back, amortised constant each. Then it inserts every number at the front, linear each, so the whole job grows like n squared.
#include <chrono>
#include <iostream>
#include <vector>
using namespace std::chrono;
int main()
{
for (int n : {1000, 10000, 100000}) {
auto t0 = steady_clock::now();
std::vector<int> back;
for (int i = 0; i < n; i++) back.push_back(i);
auto t1 = steady_clock::now();
std::vector<int> front;
for (int i = 0; i < n; i++) front.insert(front.begin(), i);
auto t2 = steady_clock::now();
std::cout << "n = " << n << ": push_back " << duration_cast<microseconds>(t1 - t0).count()
<< " us, insert at front " << duration_cast<microseconds>(t2 - t1).count() << " us\n";
}
}
n = 1000: push_back 7 us, insert at front 25 us
n = 10000: push_back 56 us, insert at front 1943 us
n = 100000: push_back 472 us, insert at front 308680 us
These numbers are one run on Compiler Explorer's GCC 12.2 at the runner's flags, g++ -O2 -std=c++17, on 2026-10-07. Your run on the Playground will print different numbers, because machines differ. The shape will be the same. us is microseconds, millionths of a second.
Read the chart against the promises. Ten times the elements made push_back about eight to ten times slower: linear in total, constant per call. Inserting at the front got 78 times slower, then 159 times slower: each insert is linear, so the whole job grows like n squared. At 100,000 elements it took about 650 times longer than push_back. Nothing in the program changed except n, and the page told you this would happen.
"Since C++20" and the compiler you actually run
The C++ standard is revised every three years: C++11, 14, 17, 20, 23. Reference pages mark when something arrived, with a note such as (since C++20) beside a signature or a small (C++20) beside a name. This track teaches C++17, which every university lab and contest judge you are likely to meet supports.
Where C++20 makes something better, the lesson shows it in a separate box titled "In C++20", right after the C++17 version. Its code runs on the Playground at -std=c++20. You will see the first one just below, and one in most modules from Module 2 on.
#include <iostream>
#include <set>
int main()
{
std::set<int> seen = {30, 10, 20};
if (seen.count(20) > 0) std::cout << "20 is in the set\n";
}
20 is in the set
In C++17 a set has no "contains" function, so you count the value. A set holds each value at most once, so the count is 0 or 1. It works, but it reads like a trick.
Run in CompilerIn C++20
set and map gained contains(), marked (since C++20) on their reference pages. The count-and-compare above becomes one call that says what it means.
#include <iostream>
#include <set>
int main()
{
std::set<int> seen = {30, 10, 20};
if (seen.contains(20)) std::cout << "20 is in the set\n";
}
20 is in the set
One more honest line. A "since C++20" marker says what the standard contains, not what your compiler has finished building. The Playground runs GCC 12, and GCC 12 does not have everything C++20 promises. The best-known gap is std::format, a C++20 way to build formatted text, which arrived in GCC 13. When a lesson wants something like that, it shows the C++17 way and names the newer one with the compiler it needs. cppreference's "compiler support" page lists, for each feature, the first GCC version that has it.
Where this is used
- cppreference.com. The reference working C++ programmers keep open, one page per function, with its signatures, complexity and the standard each piece arrived in. Every cost this track quotes can be checked there.
- The ISO working drafts. N4659 (C++17) and N4861 (C++20), and eel.is/c++draft for the latest. When two people disagree about what the STL promises, this is the text that settles it.
- cppreference's compiler support tables. One table per standard, one row per feature, one column per compiler. It is how you find out that
std::formatneeds GCC 13 before you try it on GCC 12. - The Progsity Playground. GCC 12 at
-O2 -std=c++17, or-std=c++20for an "In C++20" box. Measuring on it, as Example 3 does, is how you check a complexity line on the machine you actually use.
Common mistakes
1. Calling insert with a position number instead of an iterator.
std::vector<int> v = {1, 2, 3};
v.insert(2, 99);
GCC 12 says error: no matching function for call to 'std::vector<int>::insert(int, int)', then lists every form it tried. Look at the signatures: pos is an iterator, never an int. Write v.insert(v.begin() + 2, 99);. You will do this because "insert at 2" sounds like a number.
2. Reading "amortised constant" as "every call is fast".
In Example 2, the push that took the capacity from 64 to 128 copied 64 elements, and later moves copy far more. Sometimes one slow call matters, say in a game that must draw a frame every 16 milliseconds. Then call v.reserve(n) first, so the vector never moves during the frame. Amortised is a promise about the total, not about each call.
3. Trusting a "since C++20" marker on the Playground.
#include <format>
#include <iostream>
int main() {
std::cout << std::format("{} items\n", 3);
}
Even at -std=c++20, GCC 12 says fatal error: format: No such file or directory. The header does not exist in GCC 12's library, because std::format arrived in GCC 13. Check the compiler support table before you reach for something new, and write std::cout << 3 << " items\n"; here.
Open the cppreference page for std::vector<T>::push_back. Find its complexity line and its iterator invalidation paragraph, and write each down in your own words.
Check yourself. The complexity is amortised constant. The invalidation rule matches insert's: if the vector grew past its capacity, every iterator is invalid; otherwise only the end iterator is.
Run Example 3 on the Playground and copy your three lines. Then work out two ratios: push_back at 100,000 divided by push_back at 10,000, and the same for insert at the front.
Rules. Run it twice and say how much your numbers moved between runs.
Check yourself. The first ratio should be near 10 and the second far bigger, near 100 if the machine is steady. If both are near 10, check that your second loop inserts at front.begin().
Using only the complexity lines, predict roughly how long Example 3's insert at the front would take at n = 1,000,000, from the 100,000 number. Do not run it: on the Playground it would hit the time limit.
Rules. Show the one multiplication you used, and say which complexity line justifies it.
Check yourself. The whole job grows like n squared, so ten times n is about a hundred times the time. From 308,680 us that predicts about 30 seconds. Then explain why push_back would still take well under a hundredth of a second.
Common doubts
Is cppreference official?
No. It is a community wiki, written from the standard and checked by many readers. It is accurate enough that professionals rely on it every day, and when it matters the working draft is the final word.
Why does this track teach C++17 when C++23 exists?
Because C++17 runs everywhere you will be tested: university labs, contest judges and the Playground. C++20 is shown in its own box wherever it helps, and the newer parts arrive in your compilers over the next few years.
Will my measurements match the ones in the lesson?
The numbers will not, and they should not. Machines differ, and even two runs on one machine differ a little. What should match is the shape: how each time grows when n grows ten times.
Do I need to read the whole page every time?
No. Read the signatures to see how to call it, the complexity to see what it costs, and the invalidation note whenever you keep iterators around. The rest is there when you need it.
Key takeaways
- cppreference is the page to keep open; the ISO working drafts are the final word.
- Four parts of a page matter first: the signatures, the parameters, the complexity and the invalidation note.
- A complexity line is a promise about growth, and you can check it by growing n.
- Amortised constant means constant on average over many calls, with rare slow calls.
- "Since C++20" is about the standard; GCC 12 on the Playground lacks some of it, such as
std::format.
That completes the map. Module 1 now teaches the few C++ pieces the rest of the track uses, starting with the same program written in C and in C++.
Module test
Ten questions on this module. Pass at 70%, and you can take it as many times as you like.
Take the module testEnd of lesson 5
Mark it done, and your progress moves with you.
Next: Module Test: What the STL Is