A Tour of C++
1. The Basics
-
Initialization
double d1 = 2.3; double d2 {2.3}; double d3 = {2.3}; complex<double> z = 1; complex<double> z2 {d1,d2}; complex<double> z3 = {d1,d2};
-
Use
auto
to have the compiler deduce the variable’s type: -
Immutable values
-
Loops
-
Range over a collection:
for (auto x : v) cout << x << '\n';
-
This copies each element of
v
intox
though. To do this by reference instead:for (auto &x : v) cout << x << '\n';
-
-
Functions copy arguments by default, use explicit references to avoid this
- Use
const
references to avoid copying when you don’t want to mutate args
- Use
-
nullptr
, notNULL
-
if
statements can introduce variablesvoid do_something(vector<int>& v) { if (auto n = v.size(); n!=0) { // ... } }
-
References vs. pointers
-
Initialization vs. assignment *
In general, for an assignment to work correctly, the assigned-to object must have a value. On the other hand, the task of initialization is to make an uninitialized piece of memory into a valid object.
2. User-Defined Types
Structs
Classes
- Basic structure:
-
There is no fundamental difference between a struct and a class; a struct is simply a class with members public by default. For example, you can define constructors and other member functions for a struct.
Unions
- The compiler doesn’t keep track of which union field is currently active, so unions are typically combined with a type enum, called a tagged union
- Use
variant
s to do this directly:variant<int, string> v = "abc"; if (holds_alternative<int>(v)) { // v is an int } else { // v is a string }
Enums
enum class Color { red, blue, green };
Color col = Color::red;
3. Modularity
-
Header files containing declarations but not implementations
-
The use of
#includes
is a very old, error-prone, and rather expensive way of composing programs out of parts. If you#include
header.h in 101 translation units, the text of header.h will be processed by the compiler 101 times. If you#include
header1.h before header2.h the declarations and macros in header1.h might affect the meaning of the code in header2.h. If instead you#include
header2.h before header1.h, it is header2.h that might affect the code in header1.h
-
Modules are a replacement that will be part of C++20
- Only compiled once, even with multiple
import
s - Doesn’t need the header/impl split, control visibility from a single file
- Only compiled once, even with multiple
-
Namespaces
- Use
::
as an explicit namespace qualifier using
to bring a namespace into (lexical) scopenamespace
to define a namespace/members
- Use
-
try/catch for error handling
- Destructors are invoked as the call stack is unwound
- Exceptions appear to all inherit from
class Exception
- Use the
noexcept
modifier for functions that should never throw (the process is terminated if the function throws)
-
The
assert
macro only performs assertions in “debug mode” -
static_assert
for compile-term assertions (argument must evaluate to aconstexpr
) -
Does the act of passing a reference by value copy the reference itself or the target?
-
Return-by-reference either using pointers (bad form, apparently) or move constructors
-
Destructuring/binding struct args/returns:
struct Entry { string name; int value; }; Entry foo() { string s = "foobar"; int i = 0; return {s, i}; } void bar(Entry e) { auto [n, v] = e; }
4. Classes
-
C++ has the equivalent of pointer/value receivers using the
const
qualifier against a method:class Example { double x, y; // This method can't modify the implicit `this` double foo() const { return x; } // This method _can_ modify the implicit `this` double bar() { return y; } }
-
new
- Use
Example e {args};
to create an object that goes out of scope when the current lexical block is closed - Use
Example e = new Example(args)
to create a pointer to an object that needs to be manuallydelete
d
- Use
-
Inlining
- Functions defined in a class are inlined by default.
- It is possible to explicitly request inlining by preceding a function declaration with the keyword inline.
-
Destructors are defined as
~Class()
-
Use
delete
to collect a single object,delete[]
to collect an array -
RAII: use
new
behind abstractions so allocations occur in a constructor and deletions happen automatically in a destructor -
Accept
std::initializer_list
in a constructor to be able to do this:class Vector { public: Vector(std::initializer_list<double>); }; Vector v1 = {1, 2, 3, 4, 5}; Vector v2 = {1.23, 3.45, 6.7, 8};
Abstract Classes
-
Virtual functions are allowed to be redefined in subclasses
- An
= 0
at the end denotes a “pure virtual” function, which must be redefined - A class with a pure virtual function can’t be instantiated and is called an abstract class
- An
-
Use
:
to subclass:class Container { public: virtual void inspect() { std::cout << "HELLO!\n"; } virtual int foo() = 0; }; class LinkedList : public Container { int foo() { return 0; } };
-
Use the
override
keyword to make redefinition explicit -
Functions can be passed pointers/references to abstract types but not directly by value
-
vtables
- Given a reference to an abstract type, how does the compiler know which concrete implementation is backing it, say for a method call?
- Every concrete subtype of an abstract type has a virtual function table (or vtable) defined, which points to all concrete implementations that that type defines for virtual definitions in a supertype
- Every object links to the right vtable for its concrete type, so an abstract reference can find the right method to call
-
Abstract classes typically define a virtual destructor, so if a pointer to the abstract class is deleted, the correct concrete destructor is called.
-
dynamic_cast<Type>()
for runtime type checking *If the cast is successful,
dynamic_cast
returns a value of type new-type. If the cast fails and new-type is a pointer type, it returns a null pointer of that type. If the cast fails and new-type is a reference type, it throws an exception that matches a handler of type std::bad_cast -
Pointers aren’t destroyed when they go out of scope, so they’re typically wrapped in a value class (RAII, instantiate without
new
), with the pointers deleted in the destructor.unique_ptr
allows doing this without defining a custom class every time.
5. Essential Operations
-
Except for the “ordinary constructor”, these special member functions will be generated by the compiler as needed.
=delete
to disable this behavior
-
A good rule of thumb (sometimes called the rule of zero) is to either define all of the essential operations or none (using the default for all).
Implicit Conversions
-
C++ looks at constructors to figure out if a conversion is possible.
// 1. Given a class like this class Vector { Vector(int size) { // Initialize vector } }; // 2. This code will call that constructor Vector v1 = 7;
-
Use the
explicit
keyword against the constructor to avoid this sort of implicit conversion
Copy
-
The default copy constructor individually copies each member, which can be invalid in some cases (like for classes that maintain a pointer to data on the heap).
-
Copy constructors vs. copy assignment
class Shape { public: Shape(int n) : n {n} {} // Copy constructor Shape(Shape& s) { cout << "COPY CONSTRUCTOR" << "\n"; this->n = s.n; } // Copy assignment Shape& operator=(Shape& s) { cout << "COPY ASSIGNMENT " << "\n"; this->n = s.n; return *this; } int n = 50; }; int main() { Shape s1{90}, s2{s1}, s3{500}; s3 = s2; cout << s1.n << " " << s2.n << " " << s3.n << " \n"; }
Shape s2{s1}
invokes the copy constructors3 = s2
invokes copy assignment
Move
- A function that wants to return local data can (typically) only return a value (which is copied) or pointer.
- Use a move constructor / move assignment to avoid this, which then (transparently) changes return values to be moves instead of copies.
- Move constructor:
Vector(Vector&& a)
- Move assignment:
Vector& operator=(Vector&& a)
- Move constructor:
- The
&&
is not a reference to a reference. A reference isn’t a standalone entity like a pointer but simply an aliasing mechanism, so this doesn’t make any sense. Instead:&
is an lvalue reference: a reference that can appear on the left side of an=
; a reference that can be assigned to&&
is an rvalue reference: the opposite of an lvalue reference; a reference that cannot be assigned to, like the return value of a function
- The
&&
implies that this reference isn’t assignable anywhere else, and so the move constructor can safely modify/delete/etc. it - Modern compilers use return-value-optimization to optimize this away (a move without a move constructor) where possible; use
-fno-elide-constructors
to disable this std::move
to force a move where the compiler has imperfect information
RAII
- Before resorting to garbage collection, systematically use resource handles: let each resource have an owner in some scope and by default be released at the end of its owners scope.
- In C++, this is known as RAII (Resource Acquisition Is Initialization) and is integrated with error handling in the form of exceptions.
- Resources can be moved from scope to scope using move semantics or smart pointers, and shared ownership can be represented by shared pointers
- Yes, agree. I think RAII is one of the greatest inventions in programming ever a… | Hacker News
Iterators
- Use
begin
andend
to iterate a container - Iterators overload
++
tonext
for (auto p = c.begin(); p!=c.end(); ++p)
∗p = 0;
User-Defined Literals
- Use a suffix to override the implicit type of a constant
"Surprise"s
is astd::string
insteasd of aconst char[10]
123s
is of typesecond
instead ofint
6. Templates
-
Method definitions outside the main
class
scope need the template prefix: -
Monomorphized at compile time, no runtime cost
-
Each concrete version of a templated entity is a specialization
-
Templates can accept value arguments, not just types, but values must evaluate to constants:
-
C++17 can infer template parameters:
// Old pair<int, double> p = {1, 1.25}; // New pair p = {1, 1.25};
-
Functions can be templated too
-
Classes can implement
operator()
so their objects can be called like a function; these are function objects -
Lambdas are sugar over function objects
-
Lambdas that accept an
auto
parameter are effectively a generic lambda -
Use lambdas to convert any imperative code to an expression
7. Concepts and Generic Programming
Stopping here, it’s proving difficult to figure out which bits of code samples are part of the stdlib and which aren’t.