The 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 1) consists of my reading notes for chapter 1 through 7, and serves as personal bullet points for me to look back on. Hopefully, by reading the points you’ll find something useful as well.
(Ch.1 p.10)
constexprmeans “to be evaluated at compile time”, which can boost performance. When using aconstexprfunction (which should not have any side-effects and not modify non-local variables) with non-constant parameters passed in, the result will not be a constant expression. E.g.1 2 3 4
constexpr double half(double x) {return x / 2}; int num = 4.0; constexpr auto numHalf = half(num); // WRONG! "num" is non-constant, // result will not be constexpr!
C++17 If statement with initializer (Ch.1 p.15). We can initialize a variable and use it for a condition all in an “if-statement”. This helps keep variables’ scopes tighter. E.g.
1
if (auto a = arr[0]; a != 's') { /*...*/ }
If we’re testing the variable against zero, we can even omit the condition part. E.g.
1
if (auto sz = vec.size()) { /* loop entered if sz != 0 */}
- (Ch.1 p.17) “Assignment to reference doesn’t change what the reference refers to (unlike pointers) but assigns to the referenced object”. E.g.
1 2 3 4
int x = 2, y = 3; int &r1 = x; int &r2 = y; r1 = r2; // x becomes 3. r1 still points to x.
- No uninitialized reference is allowed (Ch.1 p.18).
- Unions (Ch.2 p.25) can be used when only one of the multiple candidate variables can be used at any given time.
- That said, STL’s
variant(Ch.2 p.26) since C++17 can replace most use-cases of unions. It is a type-safeunion. - We can use
try-catchorstd::holds_alternativein conjunction withstd::get<type>to obtain the desired value.
- That said, STL’s
- Plain
enumv.s.enum class. The latter is preferred (Ch.2):- Enumerator values of plain
enumsare implicitly converted (e.g. toint). Also, the enumerator names are “exported to the surrounding scope”, potentially causing name clashes. - In
enum class(preferred overenumdue to its type safety), the same value names can be used in differentenum class. E.g.1 2
enum class Color1 { red, green, blue }; enum class Color2 { red, yellow, black };
One can distinguish between
Color1::redandColor2::red. - However, in plain enums, this will not compile:
1 2
enum Color1 { red, green, blue }; enum Color2 { red, yellow, black }; // ERROR! Redefinition of enumerator 'red'.
- Enumerator values of plain
- Separate Compilation (Ch.3 p.30): In the example of
user.cpp,Vector.h, andVector.cpp,Vector.his the “interface” with declarations of the classes thatuser.cppwould use. The implementation details are specified inVector.cpp. Both.cppfiles “include” the.hfile, and the.cppfiles can be compiled separately. - Declarations and macros from an included header file, might affect the meaning of the later included header files (Ch.3 p.31): a significant cause of bugs!
- (Ch.3 p.34) Differences between headers (old-school
#include) and modules (export/import):- Modules are compiled once. Headers are compiled in each “translation unit” where it is used.
- Avoid problem mentioned in previous bullet point (changing include order may change meaning).
- Imports are not transitive. (i.e. X imports Y, and Z imports X. Z does not automatically have Y imported.)
- “Namespaces are primarily used to organize larger program components, such as libraries.” (Ch.3 p.35)
- A function declared as
noexceptshould “never” throw an exception. Otherwise,std::terminate()is called to terminate the program (Ch.3 p.37). - Avoid naked “new” and “delete”. Instead, keep the allocation and deallocation “buried inside the implementation of well-behaved abstractions”, i.e. constructors and destructors. (Ch.4, p.52)
- (Ch.4 p.54) “Pure virtual” functions are declared by assigning a zero. A derived class “must” define the function. E.g.
1 2 3 4
class MyClass { public: virtual int size() = 0; }
Here,
MyClassis an “abstract class” because it has a “pure virtual” function. We cannot create objects of an abstract class. Therefore, abstract classes usually don’t have “constructors” either. - In inheritance, “objects are constructed ‘base-class-first’ by constructors and destroyed ‘derived-class-first’ by destructors.” (Ch.4 p.61)
- We can use
dynamic_castto achieve “is instance of” operations. (Ch.4 p.62)1 2 3 4
template<typename Base, typename T> inline bool instanceof(T *ptr) { return dynamic_cast<Base*>(ptr) != nullptr; }
- “Use
unique_ptrorshared_ptrto avoid forgetting to delete objects created usingnew.” (Ch.4 p.64) - “Use
explicitfor constructors that take a single argument unless there is a good reason not to.” (Ch.5 p.68). Example:1 2 3 4 5 6 7
class Vector { public: explicit Vector(int sz); // avoid implicit conversion from int to Vector /* ... */ } Vector vec1(7); // OK Vector vec2 = 7; // NOT OK
std::move()is more like a cast. It doesn’t actually move anything. (Ch.5 p.72)- Range-for loops use
.begin()and.end()implicitly. (Ch.5 p.75) - (Ch.6 p.83) In C++17,
std::pair’s template arguments (types) can be deduced:1 2
std::pair<int, double> p1 = {1, 3.41}; // pre C++17 std::pair p2 = {1, 3.41}; // since C++17
It’s a pretty good read.
Read Part 2 here.