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)
constexpr
means “to be evaluated at compile time”, which can boost performance. When using aconstexpr
function (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-catch
orstd::holds_alternative
in conjunction withstd::get<type>
to obtain the desired value.
- That said, STL’s
- Plain
enum
v.s.enum class
. The latter is preferred (Ch.2):- Enumerator values of plain
enums
are implicitly converted (e.g. toint
). Also, the enumerator names are “exported to the surrounding scope”, potentially causing name clashes. - In
enum class
(preferred overenum
due 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::red
andColor2::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.h
is the “interface” with declarations of the classes thatuser.cpp
would use. The implementation details are specified inVector.cpp
. Both.cpp
files “include” the.h
file, and the.cpp
files 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
noexcept
should “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,
MyClass
is 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_cast
to 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_ptr
orshared_ptr
to avoid forgetting to delete objects created usingnew
.” (Ch.4 p.64) - “Use
explicit
for 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.