For Part 1, visit this previous post.
The selected following are some modern C++ features that I found interesting or unfamiliar at the time of reading “A Tour of C++” by Professor Bjarne Stroustrup, whose C++ course at Columbia University I am currently enrolled in.
Bjarne Stroustrup, the creator of C++ and my professor at Columbia University
This post (Part 2) consists of my reading notes for chapter 8 through 14. Hopefully, by reading the points you’ll find something useful as well.
- For each header file from C’s standard library, there’s a version “with its name prefixed by
c
and without.h
”. This version usesstd::
namespace (Ch.8 p.110).1 2 3 4 5
// C style #include <stdlib.h> // C++ style (std::) #include <cstdlib>
std::string::replace()
’s new replacement string doesn’t need to be the same length as the replaced target substring. (Ch.9 p.112)String literals are
const char*
by default. If we want typestd::string
, we need thes
suffix (Ch.9 p.113).1 2 3 4 5 6 7 8
/* * The "s" suffix needs one of the following namespace to be included: * - using namespace std::literals * - using namespace std::string_literals * - using namespace std::literals::string_literal */ auto s1 = "Ian"; // const char* (C-style) auto s2 = "Pan"s; // std::string
- “Short string optimization”: short string values are kept in the string object (special optimization), and only longer strings are placed on free store (dynamically allocated memory on the heap) as the usual case. (Ch.9 p.113)
string_view
is a read-only view of its characters (Ch.9 115).- “Raw string literals” starting with
R"(
and ending with)"
allows backslashes to be used directly in strings. E.g."\\w{2}\\d{4}"
can becomeR"(\w{2}\d{4})"
(Ch.9 p.116). - (Ch.10 p.125) In output streams, the “result of an output expression can be used for further output”. This is why we can chain multiple “put-to” (
<<
) statements. - (Ch.10 p.126) The same can be said for input streams: we can chain multiple “get-from” (
>>
) statements.- The read of an integer will stop when a character is not a digit. Also,
>>
will skip initial whitespaces by default.
- The read of an integer will stop when a character is not a digit. Also,
- We can use
istream1 >> char1
as a condition, meaning “Did we successfully read a character from istream1 into char1?” (Ch.10 p.128) istream >> ch
skips whitespace by default, butistream.get(ch)
does not (Ch.10 p.128).- Formatting with manipulators (placed between the output stream and the target to output) (Ch.10 p.129):
1
std::cout << std::hex << 1234; // "4d2"
Other manipulators include
std::oct
,std::scientific
,std::hexfloat
, etc. - (Ch. 10 p.130)
std::cout.precision()
orstd::setprecision()
can round floating numbers by a length limit. Note that it limits the length “in total”, not just after the decimal point:1 2
std::cout.precision(7); std::cout << 1234.567890; // 1234.568 (length 7 in total)
However, if the original number was an integer, “precision” has no effect:
1 2
std::cout.precision(4); std::cout << 123456789; // 123456789 (not truncated/rounded at all)
If we want to restore the default precision, we can store it before modifying it:
1 2 3 4
auto x = std::cout.precision(); std::cout.precision(4); // later restore std::cout.precision(x);
- (Ch.10 p.132) C-style I/O (printf, scanf) has better I/O performance. If we don’t use C-style I/O but care about I/O performance, we can call:
1
std::ios_base::sync_with_stdio(false);
- The file system library
<filesystem>
allows us to express file system paths and navigate through a file system, examining file types and their associated permissions. (Ch.10 p.132) std::vector
’s subscript operator does not check range – it’ll give some random value rather than throwing an error! (Ch.11 p.141) This is because checking the range implies an extra 10%-order cost:1 2
std::vector<int> v{1,2,3,4}; std::cout << v[100]; // 0. No error thrown.
- Use
.at()
to check range and throw out-of-range error:1 2
std::cout << v.at(100); // terminate called after throwing an instance of 'std::out_of_range'
- Use
std::list
are doubly-linked. Can be used over vectors if insertions and deletions are frequent (Ch.11 p.142).- There’s also a singly-linked list in STL, called
std::forward_list
(Ch.11 p.143). This can save space, since each node will only have 1 pointer instead of 2.
- There’s also a singly-linked list in STL, called
- Every STL container provides the functions
begin()
andend()
(Ch.11 p.143). std::equal_range()
returns a pair that contains the values ofstd::lower_bound()
andstd::upper_bound()
(Ch.11 p.143).std::map
(a.k.a. associative array, dictionary) is a balanced binary search tree, usually implemented by a red-black tree (Ch.11 p.144).- Use
find()
instead of[]
to avoid inserting default values with keys that don’t exist. - The cost of map lookup is O(log n).
- Use
- Emplace operations, “such as
emplace_back()
, takes arguments for an element’s constructor and builds the object in a newly allocated space in the container rather than copying an object into the container” (Ch.11 p.147):1 2 3
vector<pair<int, string>> v; v.push_back(make_pair(1, "hello")); v.emplace_back(2, "hi");
- (Ch.11 p.148) We can often create good hash functions by combining standard hash functions for elements using the exclusive-or operator (
^
). std::back_inserter()
returns an iterator (std::back_insert_iterator
) that can be used to append values to the back of a container (Ch.12 p.150). With this function, we can avoid usingrealloc()
, which is more C-style and error-prone.- Iterators can be great middlemen. STL algorithms can operate on a container’s data through iterators without knowing anything about the container (Ch.12 p.153).
- No STL algorithm adds or subtracts elements of a container (Ch.12 p.157). For that, we need container methods (
push_back()
,erase()
) or functions that knows about the container, likeback_inserter()
. - Many STL algorithms, such as find, count, replace, sort, copy, have parallel versions, invoked by passing an extra argument:
1 2 3
sort(begin(vec), end(vec)); // default, sequential sort(std::execution::seq, begin(vec), end(vec)); // default, sequential sort(std::execution::par, begin(vec), end(vec)); // parallel
- Multiple
shared_ptr
instances of an object “share” the ownership. The object will be destroyed when “the last shared pointer” is destroyed (Ch.13 p.165). Unique pointers are generally more efficient because it does not need to track the “use count” that shared pointers do. - STL containers’ “moved-from state” is “empty” (Ch.13 p.168).
- There is no time/space overhead to use
std::array
versus the built-in C-style array (Ch.13 p.171). - The second template argument (size) of
std::array
must be a constant expression (Ch.13 p.171):1 2
int n = 3; std::array<char, n> arr{'a', 'b', 'c'}; // error: size not a constant expression
We need to specify n as a “const” or a “constexpr” here, like so:
1 2 3 4 5
const int n = 3; std::array<char, n> arr{'a', 'b', 'c'}; // good constexpr int n = 3; std::array<char, n> arr{'a', 'b', 'c'}; // good
Note that we cannot use a function parameter as the size of the array, because function parameters are never constant expressions:
1 2 3 4 5 6 7
void f(const int n) { std::array<char, n> arr{'a', 'b', 'c'}; // error! } void f(constexpr int n) { // invalid constexpr! std::array<char, n> arr{'a', 'b', 'c'}; }
To solve this, we must make a template like so:
1 2 3 4 5 6 7 8 9 10
template <int n> auto func2() -> std::array<int, n> { std::array<int, n> a{1, 2, 3}; return a; } int main() { auto a = func2<3>(); // ... }
std::array
can be passed to C-style function that expects a pointer, using.data()
, or with the explicit address of its first element (Ch.13 p.171):1 2 3 4 5 6 7 8 9
void func1(int *p, int sz); void func2() { const int n = 3; std::array<int, n> a{1, 2, 3}; func1(a, n); // ERROR! func1(&a[0], n); // good func1(a.data(), n); // good }
- Advantages of
std::array
over built-in C-style array include (Ch.13 p.172):- Ease of use with STL algorithms.
- Can be copied using
=
. - No surprising conversions to pointers.
“Template argument type deduction from constructor arguments” is added in C++17 (Ch.13 p.174):
1 2 3 4
tuple<string, int> tup1{"Ian Pan", 123}; // pre C++17 tuple tup2{"Ian Pan"s, 123}; // post C++17 // Alternatively... auto tup3 = make_tuple("Ian Pan"s, 123);
- Getting an element from a tuple can be done as follows:
1 2
auto s1 = get<0>(tup1); auto s2 = get<string>(tup1);
- We also use
get<>()
to write to the tuple:1 2
get<0>(tup1) = "Columbia"; get<string>(tup1) = "University";
- Getting an element from a tuple can be done as follows:
- An
optional<T>
can be seen as a special kind of variant, like avariant<T, nothing>
(Ch.13 p.176). std::enable_if
is widely used for conditionally introducing definitions (e.g. for defining an operator member function) (Ch.13 p.184).(Ch.14 p.192) To generate random numbers, one can specify the “engine”, the “distribution” (e.g. uniform, Gaussian, exponential, etc.) and create a “generator” out of it:
1 2 3 4 5 6 7
using my_engine = default_random_engine; using my_distribution = uniform_int_distribution<>; my_engine eng{}; my_distribution dice{1, 6}; auto rollDice = [&]() { return dice(eng); }; auto num = rollDice();
std::valarray
is a vector-like container that supports element-wise mathematical operations, as well as slicing and shifting (rolling). (Basically, the superpowers we thought was unique to Python) (Ch.14 p.192)- Word of advice: “Consider
accumulate()
,inner_product()
,partial_sum()
, andadjacent_difference()
before you write a loop to compute a value from a sequence” (Ch.14 p.193).