Book-en-us
- Preface
- Chapter 01: Towards Modern C++
- Chapter 02: Language Usability Enhancements
- Chapter 03: Language Runtime Enhancements
- Chapter 04: Containers
- Chapter 05: Smart Pointers and Memory Management
- Chapter 06: Regular Expression
- Chapter 07: Parallelism and Concurrency
- Chapter 08: File System
- Chapter 10: C++20
- Chapter 11: C++23
- Chapter 12: C++26 (Outlook)
- Appendix 1: Further Study Materials
- Appendix 2: Modern C++ Best Practices
- Appendix 3: Modern C++ Feature Index
Chapter 04: Containers
4.1 Linear Container
std::array
(since C++11)
When you see this container, you will have this problem:
- Why introduce
std::arrayinstead ofstd::vectordirectly? - Already have a traditional array, why use
std::array?
First, answer the first question. Unlike std::vector, the size of the std::array object is fixed. If the container size is fixed, then the std::array container can be used first.
Also, since std::vector is automatically expanded, when a large amount of data is stored, and the container is deleted,
The container does not automatically return the corresponding memory of the deleted element. In this case, you need to manually run shrink_to_fit() to release this part of the memory.
std::vector<int> v; |
The second problem is much simpler. Using std::array can make the code more "modern" and encapsulate some manipulation functions, such as getting the array size and checking if it is not empty, and also using the standard friendly. Container algorithms in the library, such as std::sort.
Using std::array is as simple as specifying its type and size:
std::array<int, 4> arr = {1, 2, 3, 4}; |
When we started using std::array, it was inevitable that we would encounter a C-style compatible interface. There are three ways to do this:
void foo(int *p, int len) { |
std::forward_list
(since C++11)
std::forward_list is a list container, and the usage is similar to std::list, so we don't spend a lot of time introducing it.
Need to know is that, unlike the implementation of the doubly linked list of std::list, std::forward_list is implemented using a singly linked list.
Provides element insertion of O(1) complexity, does not support fast random access (this is also a feature of linked lists),
It is also the only container in the standard library container that does not provide the size() method. Has a higher space utilization than std::list when bidirectional iteration is not required.
4.2 Unordered Container
(since C++11)
We are already familiar with the ordered container std::map/std::set in traditional C++. These elements are internally implemented by red-black trees.
The average complexity of inserts and searches is O(log(size)). When inserting an element, the element size is compared according to the < operator and the element is determined to be the same.
And select the appropriate location to insert into the container. When traversing the elements in this container, the output will be traversed one by one in the order of the < operator.
The elements in the unordered container are not sorted, and the internals is implemented by the Hash table. The average complexity of inserting and searching for elements is O(constant),
Significant performance gains can be achieved without concern for the order of the elements inside the container.
C++11 introduces two unordered containers: std::unordered_map/std::unordered_multimap and
std::unordered_set/std::unordered_multiset.
Their usage is basically similar to the original std::map/std::multimap/std::set/set::multiset
Since these containers are already familiar to us, we will not compare them one by one. Let's compare std::map and std::unordered_map directly:
#include <iostream> |
The final output is:
std::unordered_map |
4.3 Tuples
Programmers who have known Python should be aware of the concept of tuples. Looking at the containers in traditional C++, except for std::pair
there seems to be no ready-made structure to store different types of data (usually we will define the structure ourselves).
But the flaw of std::pair is obvious, only two elements can be saved.
Basic Operations
(since C++11)
There are three core functions for the use of tuples:
std::make_tuple: construct tuplestd::get: Get the value of a position in the tuplestd::tie: tuple unpacking
#include <tuple> |
std::get In addition to using constants to get tuple objects, C++14 adds usage types to get objects in tuples:
std::tuple<std::string, double, double, int> t("123", 4.5, 6.7, 8); |
Runtime Indexing
If you think about it, you might find the problem with the above code. std::get<> depends on a compile-time constant, so the following is not legal:
int index = 1; |
So what do you do? The answer is to use std::variant<> (introduced by C++ 17) to provide type template parameters for variant<>
You can have a variant<> to accommodate several types of variables provided (in other languages, such as Python/JavaScript, etc., as dynamic types):
#include <variant> |
So we can:
int i = 1; |
Merge and Iteration
Another common requirement is to merge two tuples, which can be done with std::tuple_cat:
auto new_tuple = std::tuple_cat(get_student(1), std::move(t)); |
You can immediately see how quickly you can traverse a tuple? But we just introduced how to index a tuple by a very number at runtime, then the traversal becomes simpler.
First, we need to know the length of a tuple, which can:
template <typename T> |
This will iterate over the tuple:
for(int i = 0; i != tuple_len(new_tuple); ++i) |
That said, traversing a tuple by "first implementing runtime indexing, then indexing element by element" works but is rather roundabout. If all you want is to apply the same operation to every element, the more direct and idiomatic way is to expand the indices at compile time with std::index_sequence (introduced in C++14). In C++17, it can be combined with a fold expression:
template <typename Func, typename Tuple, std::size_t... idx> |
In C++20, we can further drop the helper function by using a lambda that allows explicitly written template parameters:
template <typename Func, typename... Args> |
The call site is then very straightforward, and no runtime indexing is needed beforehand:
iterate_tuple([](const auto& v) { std::cout << v << ' '; }, new_tuple); |
4.4 std::string_view and std::byte
std::string_view
(since C++17)
std::string_view, introduced in C++17, is a non-owning, read-only view over a sequence of characters; it holds just a pointer and a length. Declaring a parameter as std::string_view accepts both a std::string and a string literal, without any copy or allocation:
#include <string_view> |
Mind its lifetime: a string_view does not own the underlying data, so the referenced character sequence must outlive the view, otherwise you get a dangling reference.
std::byte
(since C++17)
std::byte represents a single byte of raw memory. Unlike char or unsigned char, it is not an arithmetic type — the standard defines only bitwise operators for it, which prevents accidental arithmetic on raw bytes at the type level:
#include <cstddef> |
4.5 Associative container improvements
(since C++17)
C++17 added several more precise and more efficient operations to associative containers such as std::map / std::unordered_map:
try_emplace: inserts only when the key is absent; when the key already exists it does not modify the existing value nor move from its arguments, which makes it better thanemplacefor "insert if not present".insert_or_assign: inserts a new element, or overwrites the value when the key already exists, returning whether an insertion happened.- Node-based operations
extract/merge:extractdetaches a node from the container without copying or moving the element, andmergesplices nodes from another container directly in.
#include <map> |
4.6 Polymorphic allocators std::pmr
(since C++17)
C++17 introduced the std::pmr namespace in <memory_resource>, providing polymorphic allocators based on a memory resource. It decouples the where to allocate policy from the container type: the same pmr container backed by different memory resources is still a single type, avoiding the type bloat caused by template allocators.
For instance, std::pmr::monotonic_buffer_resource can allocate from a pre-prepared buffer (even one on the stack) and frees everything only when the resource is destroyed — ideal for allocation-heavy workloads with a shared lifetime:
#include <array> |
Conclusion
This chapter briefly introduces the new containers in modern C++. Their usage is similar to that of the existing containers in C++. It is relatively simple, and you can choose the containers you need to use according to the actual scene, to get better performance.
Although std::tuple is effective, the standard library provides limited functionality and there is no way to meet the requirements of runtime indexing and iteration. Fortunately, we have other methods that we can implement on our own.
Changkun Ou © 2016-2026. The book is licensed under Creative Commons Attribution-NonCommercial-NoDerivatives 4.0, code is open sourced under the MIT License.
If you like the book, you could donate the author.