Learn C++ STL

Lesson 2 of 6 ¡ What the STL Is, and Why It Changes How You Write C++

Module 0 ¡ What the STL Is, and Why It Changes How You Write C++

Why It Matters: Fewer Lines, Fewer Bugs, Known Costs

FreeReading

In this lesson

  • Give three honest reasons the STL is worth learning, each backed by a program.
  • Compare a C program with its STL version and count what you no longer write by hand.
  • Name what the STL costs you, so you can decide when hand-written C is still right.

Bob wrote a growing array in C for his marks program. It works on Monday. On Tuesday a friend adds six marks and two of them come back as zeros.

Bob's resize copies one element too few, so every time the array grows, the last mark is lost. The compiler said nothing, and the program never crashed. vector::push_back does the same job in millions of programs, and a bug like Bob's would have been found and fixed long ago.

That is the first reason the STL matters. There are two more, and there are costs too. This lesson gives you all of them, because a library that only sells itself is not telling you the whole story.

Reason one: correct code you do not have to write

Every line you write is a line that can be wrong. The STL's containers and algorithms ship with every C++ compiler, and the same code is used by millions of programs every day. A bug in push_back would be found within hours, by someone else.

Here is the same job twice: store the squares of 1 to 5 in a growing array, then print them. First in C, done properly, with the growth and the failure check.

Example 1: a growing array in C
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *a = NULL;
    size_t n = 0, cap = 0;
    for (int x = 1; x <= 5; x++) {
        if (n == cap) {
            cap = cap ? cap * 2 : 1;
            int *p = realloc(a, cap * sizeof *a);
            if (p == NULL) { free(a); return 1; }
            a = p;
        }
        a[n++] = x * x;
    }
    for (size_t i = 0; i < n; i++) printf("%d\n", a[i]);
    free(a);
    return 0;
}
1
4
9
16
25

This is correct C. It keeps a count and a capacity, doubles the capacity when full, checks that realloc worked, and frees the memory at the end. Each of those is a place Bob's kind of bug can live.

Run in Compiler
Example 2: the same job with a vector
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> a;
    for (int x = 1; x <= 5; x++) {
        a.push_back(x * x);
    }
    for (int v : a) std::cout << v << '\n';
}
1
4
9
16
25

Same output. The count, the capacity, the growth rule, the failure check and the free are all still happening, inside std::vector. You just no longer write them. So the bugs that lived in those lines have nowhere to live.

Run in Compiler

Reason two: every operation has a known cost

The C++ standard does not only say what push_back does. It also says how long it may take, as a complexity (how the work grows as the container grows). For push_back the promise is "amortised constant": on average, adding one element costs the same whether the vector holds ten or ten million.

Your own C array comes with no such promise unless you work it out. The STL's promises are written down, the same for every compiler, and you can look them up before you write a line. Lesson 5 shows you where.

Here is a search, twice. Find where 90 is in a list of marks.

Example 3: a linear search in C
#include <stdio.h>

int main(void)
{
    int marks[] = {72, 45, 90, 61, 88};
    int n = sizeof marks / sizeof marks[0];
    int where = -1;
    for (int i = 0; i < n; i++) {
        if (marks[i] == 90) { where = i; break; }
    }
    if (where >= 0) printf("90 is at index %d\n", where);
    else printf("90 is not there\n");
    return 0;
}
90 is at index 2

The loop is short, but you chose the bounds, the "not found" value of -1, and the break. Get any of the three wrong and the answer is wrong.

Run in Compiler
Example 4: the same search with std::find
#include <algorithm>
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> marks = {72, 45, 90, 61, 88};
    auto it = std::find(marks.begin(), marks.end(), 90);
    if (it != marks.end())
        std::cout << "90 is at index " << (it - marks.begin()) << '\n';
    else
        std::cout << "90 is not there\n";
}
90 is at index 2

std::find returns an iterator to the first match, or end() when there is none, so "not found" is never a number you invent. Its cost is written in the standard: at most one comparison per element, which is linear. auto asks the compiler to write the iterator's type for you; Module 1 covers it.

Run in Compiler

Reason three: code other people can read

When a C++ programmer reads std::sort(v.begin(), v.end()), they know what it does, what it costs and that it is correct. They do not have to read a sorting function first. The names are shared by every C++ programmer on earth, the way printf is shared by every C programmer.

Sorting shows this best. In C you write a comparison function that takes void pointers, casts them, and returns a negative, zero or positive number.

Example 5: sorting with qsort in C
#include <stdio.h>
#include <stdlib.h>

int by_value(const void *a, const void *b)
{
    int x = *(const int *)a, y = *(const int *)b;
    return (x > y) - (x < y);
}

int main(void)
{
    int marks[] = {72, 45, 90, 61, 88};
    size_t n = sizeof marks / sizeof marks[0];
    qsort(marks, n, sizeof marks[0], by_value);
    printf("sorted:");
    for (size_t i = 0; i < n; i++) printf(" %d", marks[i]);
    printf("\n");
    return 0;
}
sorted: 45 61 72 88 90

The comparator avoids the classic return x - y;, which overflows for very large and very small ints. A reader still has to check the casts, the element size and the count before trusting it.

Run in Compiler
Example 6: the same sort with std::sort
#include <algorithm>
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> marks = {72, 45, 90, 61, 88};
    std::sort(marks.begin(), marks.end());
    std::cout << "sorted:";
    for (int m : marks) std::cout << ' ' << m;
    std::cout << '\n';
}
sorted: 45 61 72 88 90

No size, no casts, no comparator for the usual order. The compiler knows the element type, so a mistake such as sorting strings with an int comparison is a compile error, not a wrong answer.

Run in Compiler

The three pairs, counted

Here are the six programs above, measured two ways. The first is lines of code, not counting blank lines. The second is the details you must get right by hand, each named in the table so you can check the count yourself.

Lines of code for the three pairs, C against the STL Lines of code (blank lines not counted), one unit = 20 px Growing array C: 19 vector: 10 Search C: 13 std::find: 12 Sort C: 17 std::sort: 11 Grey bars are the C programs, coloured bars the STL ones. Includes and braces count as lines.
Figure 1. Lines of code in Examples 1 to 6, drawn to scale. The gap is biggest where C had the most bookkeeping.
PairDetails you get right by hand in CWith the STL
Growing array5: the count, the capacity, the growth rule, the realloc failure, the free0
Search4: the element count, the loop bounds, the "not found" value, the break1: comparing with end()
Sort4: the element count, the element size, the void pointer casts, the sign convention0

Lines are a rough measure, and the gap is modest for a search. The second column is the one that matters. Thirteen hand-kept details become one, and each detail removed is a bug that can no longer happen.

What the STL costs you

Nothing is free. Here are the four costs, said plainly, so you can weigh them.

  • Compile time. On Compiler Explorer's GCC 12.2 at the runner's flags plus -E, which only pastes the headers in, Example 1 with its two C headers comes to 665 lines. Example 2 with <iostream> and <vector> comes to 32,394 lines. Big projects feel this as slower builds.
  • Error messages. When you misuse a template, the message can run to dozens of lines of library internals. Module 1 teaches you to find the one line that matters.
  • A learning curve. You must learn which container suits which job, and what each operation costs. That is what this track is for, and it takes weeks, not an afternoon.
  • Less control. A vector decides how much to grow and when to move its elements. You can ask it to reserve space in advance, but you cannot pick its growth rule. Code that must control every byte, such as firmware for a tiny device, often uses plain C arrays for this reason.

So the honest summary is this. For almost every program you will write in this track, in a contest or at work, the STL wins. When you hit a case where it does not, you will know why, because you know its costs.

Where this is used

  • The C++ Core Guidelines. The guidelines edited by Bjarne Stroustrup and Herb Sutter say to prefer the standard containers: rule SL.con.1 prefers std::array or std::vector to a C array, and SL.con.2 makes vector the default.
  • LLVM's ADT library. LLVM wrote some containers of its own, such as SmallVector, and its Programmer's Manual says why. A SmallVector keeps its first few elements inside itself, which avoids a heap allocation for small sizes. That is the "less control" cost being bought back, on purpose, by a project that measured it.
  • Google's C++ Style Guide. Google's code uses the standard containers and algorithms freely. The guide bans only a short list of standard library parts, such as <ratio> and <filesystem>, rather than the STL itself.

Common mistakes

1. Believing the STL removes every bug.

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v = {10, 20, 30};
    std::cout << v.at(10) << '\n';
}
terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 10) >= this->size() (which is 3)

The STL removes the bookkeeping bugs, not the logic bugs. Asking for element 10 of a three-element vector is still wrong. v.at(10) stops the program with the message above, which is the good outcome. v[10] does no check at all and reads whatever memory is there. Use at() while you are learning.

2. Writing your own growing array, and copying one element too few.

void push(int x)
{
    if (n == cap) {                         /* full: grow by two */
        int *bigger = calloc(cap + 2, sizeof *bigger);
        for (int i = 0; i < n - 1; i++) bigger[i] = a[i];
        free(a);
        a = bigger;
        cap += 2;
    }
    a[n++] = x;
}

This is Bob's function. GCC gives no message at all. Push 1 to 6 and print, and the output is 1 0 3 0 5 6: every time the array grows, the last element is lost, because the copy stops at n - 1. The fix is i < n, or better, std::vector. You will write this bug because off-by-one is the most common bug there is.

3. Treating std::find's answer as true or false.

if (std::find(v.begin(), v.end(), 8)) {
    std::cout << "found\n";
}

GCC 12 says, shortened in the middle, error: could not convert 'std::find<...>(...)' from '__gnu_cxx::__normal_iterator<int*, std::vector<int> >' to 'bool'. find returns an iterator, not a yes or no. Write if (std::find(v.begin(), v.end(), 8) != v.end()). The C habit of "zero means no" is why this one catches people.

Brain teaser

This program is valid C and valid C++. Compile it as C, then as C++. Does it print the same number both times?

#include <stdio.h>

int main(void)
{
    printf("%zu\n", sizeof('a'));
    return 0;
}

The question is what type 'a' has. Look up "character literal" for each language, and remember how big an int is on the Playground.

Exercise 1Easy

Run Examples 1 and 2 on the Playground. Then change both so they store the squares of 1 to 8 instead of 1 to 5.

Check yourself. Count the lines you had to change in each program. In the C version the growth code did not change, and you should be able to say why: it already handled any count.

Exercise 2Medium

Take Bob's push from Common mistakes. Without running it, trace pushes 1 to 6 on paper: write n, cap and the array's contents after each push.

Rules. Use calloc's promise that new memory starts as zeros. Mark the pushes where the array grows.

Check yourself. Your last line should match 1 0 3 0 5 6. The array grows on pushes 1, 3 and 5, and only two of those three lose an element. Explain why the first one does not.

Exercise 3Hard

Kenji is writing firmware for a sensor with 2 KB of memory and no operating system. Write one paragraph on whether he should use std::vector, using the four costs above.

Rules. Name at least two of the costs and say whether each one matters on his device.

Check yourself. A good answer focuses on control. A vector asks the heap for memory whenever it grows. On 2 KB with no system, that request can fail with no way to recover. Compile time hardly matters there. A fixed-size array, or std::array from Lesson 3, is the usual choice.

Common doubts

  • If the STL is so good, why do C programmers still write their own arrays?

    C has no STL, so in C there is no choice. In C++ there is. The usual reasons to write your own are under "What the STL costs you": tight memory, full control, or a need like LLVM's SmallVector.

  • Is std::sort really faster than qsort?

    Usually, yes. qsort calls your comparator through a function pointer for every comparison. std::sort is a template, so the compiler sees the comparison and can build it straight into the loop. You will measure this kind of thing in Module 17.

  • Does a bigger compile mean a slower program?

    No. The 32,394 lines are declarations the compiler reads; almost none of them turn into code in your program. They cost build time, not run time.

  • Can I trust the STL in a contest?

    Yes. ICPC and Codeforces both offer GCC with its standard library, the one the Playground runs, and contest solutions use it all the time. The costs that matter in a contest are the complexities, which this track teaches for every operation.

Key takeaways

  • The STL gives you tested code: the bookkeeping lines where bugs live are gone from your program.
  • Every STL operation has a cost written in the standard, so you can know it before you run anything.
  • Shared names make code readable: every C++ programmer knows what std::sort does.
  • The costs are real: longer compiles, long error messages, a learning curve and less control.
  • The STL removes bookkeeping bugs, not logic bugs; at() catches a bad index, [] does not.

Next comes the full list of containers, in one table, with the one question each answers fastest.

End of lesson 2

Mark it done, and your progress moves with you.

Next: The Containers at a Glance: What Each One Is Good At