đŸŒ± Tim's Dev Wiki

Search IconIcon to open search

C++

Last updated December 27, 2022.

C++ is a statically-typed, low-level programming language that supports object-oriented programming. It’s frequently used in any software system that requires resource efficiency such as operating systems, game engines, databases, compilers, etc.

C/C++’s high performance is attributed to how closely it’s constructs and operations match the hardware.

The ISO C++ standard defines:

Also see C++ standard library.

# Basics

# Copy, List and Direct Initialisation

There are a few ways to initialise a variable with a value.

  1. Copy initialisation: using =. It implicitly calls a constructor.
  2. List initialisation, also called uniform initialisation: using { }.
  3. Direct initialisation: using ( ). Think of the parentheses as being used to directly invoke a specific constructor.

Prefer uniform initialisation over copy initialisation.

```cpp
int b(1);     // Direct initialisation.
int a{1};     // List initialisation.
int c = 1;    // Copy initialisation.
int d = {1};  // Copy/List initialisation.
```

# Designated Initialisers

C++20 introduces a new way to initialise the members of a class/struct:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct Human {
  string name;
  int age;
};

int main() {
  Human andrew{ .name = "Andrew", .age = 42 };
  Human linus{ .name{"Linus"} };               // You can also use list initialisation on the members.

  Human ada{ .age = 36, .name = "Ada" };       // Error. You must initialise the fields in the same order as they're declared in the struct/class.
  return 0;
}

# Pointers and References

Pointers and references are really the same thing under the hood, however they have different semantics to the programmer. You can consider references as syntactic sugar for pointers whose main purpose is to help you write cleaner code, compared to if you were to use pointers for the same use case.

Unlike other languages, in C++, arguments are always passed by value by default unless the function signature explicitly says it takes in a pointer or reference. This means functions will entirely copy all the objects you pass in, unless you pass in a pointer/reference.

* and & have different meanings depending on whether they appear in a type declaration (LHS) or whether they appear in an expression that is to be evaluated (RHS).

In a type declaration:

In an expression:

# Pointers

Pointers are just memory addresses, often to the contents of an object allocated on the heap.

1
2
3
4
5
int x = 2;
int y = 3;
int* p = &x;
int* q = &y;
p = q;          // p now contains the memory address of y.

illustration of pointers|590

# References

You can think of a reference variable as an alias for another variable. They don’t occupy any memory themselves, once your program is compiled and running.

1
2
3
4
5
int x = 2;
int y = 3;
int& r = x;
int& r2 = y;
r = r2;      // Remember, you can think of references as aliases. This assignment is basically just `x = y`

illustration of references|600

# new

new is used to instantiate classes and arrays on the heap. All objects allocated with new must have a corresponding delete somewhere.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Human {
public:
    Human() {};
};

int main() {
    // Creating an object whose memory will be allocated on the heap.
    Human* me = new Human();
    delete me;

    // Creating an array whose memory will be allocated on the heap.
    int* A = new int[3];
    delete A;

    // This array will have its memory allocated on the stack, so no delete operation is necessary.
    int B[3];             
}

# delete

There are two delete operators, delete and delete[].

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public:
    string* courses;
    string* zId;

    Student() {
        courses = new string[3];
        zId = new string("z5258971");
    }

    ~Student() {
        delete[] courses;       // Deleting an array.
        delete zId;             // Deleting an individual string.
    }
};

# Type Qualifiers: auto, const, constexpr, static

# Auto

When specifying the data type of something as auto, C++ automatically infers the type.

# Const

The const qualifier makes it ‘impossible’ to assign a new value to a variable after it’s initialised. There is 0 negative performance impact of enforcing const since it’s all done at compile-time. Using const can actually allow the compiler to make optimisations.

Prefer making things const by default. See const correctness for a pitch on why.

# Const Pointers

1
2
3
const int *p;               // A pointer to an immutable int.
const int * const q = ...;  // An immutable pointer to an immutable int. It must be initialised with a memory address.
int * const r = ...;        // An immutable pointer to an int. It must be initisalised with a memory address.

If this is hard to read, see the clockwise-spiral rule.

# Const References

Typing a variable as a const reference makes it a read-only alias. It’s especially helpful for function parameters.

Prefer typing function parameters as const references. This gives the caller confidence that what they pass in is not modified in any way.

If you don’t want a function to modify a caller’s argument, you have these options:

1
2
3
4
5
6
7
void foo1(const std::string& s);   // Preferred approach.
void foo2(const std::string* s);   // A pointer to a const also works.
void foo3(std::string s);          // Since pass-by-value is the default, `s` is an independent copy of what the caller passed in.

// If you want a parameter to be modifiable:
void bar1(std::string& s);         // This might modify the caller's string directly.
void bar1(std::string* s);         // So can this.

You can’t have 100% certainty that what you pass as a const reference is unchanged. See this example from isocpp:

# Const Methods

Const methods can only read this and never mutate any of its members. The this pointer will essentially become a pointer to const object. To specify a const method, the const qualifier must be placed after the parameter list.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Student {
public:
  // ...
  void my_const_func() const {       
    this->name = "Overridden";   // Compiler error!
  }

private:
  string name;
};

Methods that don’t modify object state should be declared const. See this const-correctness article

What about making methods return const values, eg. const Foo bar();? It’s mostly pointless. However, it is not pointless if you’re returning a pointer or reference to something that is const.

# Mutable

Declaring a method with const will cause a compiler error to be raised for when that method attempts to change a class variable.

You can add the mutable keyword to allow an exception for what class variables can be modified by const member functions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Student {
public:
  // ...
  void myConstFunction() const { 
    this->name = "Overridden";       // This is now fine ✓.
  }

private:
  mutable string name;              // Permit `name` to be mutated by const member functions.
};

# Constexpr

The constexpr type specifier is like const, except the RHS value must be able to be determined at compile-time.

1
2
const int a = some_val;
constexpr int b = 42;

# Constexpr Functions

Constexpr functions are those than can be executed at compile-time, meaning all its state and behaviour is determinable at compile-time.

# Static

# Static Variables

Inside functions, static variables let you share a value across all calls to that function.

1
2
3
4
void foo() {
    static int a = 42;    // All calls to **foo** will see **a = 42**.
    ...                   // If **a** changes, then all calls to **foo** will see that change too
}

Static function variables are generally considered bad because they represent global state and are therefore much more difficult to reason about *.

# Clockwise-Spiral Rule

Clockwise-Spiral Rule is a trick for reading variable types.

  1. Start at the variable name.
  2. Follow an outwards clockwise spiral from that variable name to build up a sentence.

Example: clockwise-spiral rule example|400 Starting at the name fp:

  1. fp is a pointer.
  2. fp is a pointer to a function (that takes in an int and a float pointer).
  3. fp is a pointer to a function (that takes in an int and a float pointer) that returns a pointer.
  4. fp is a pointer to a function (that takes in an int and a float pointer) that returns a pointer to a char.

More examples:

1
2
3
4
int *myVar;                     // pointer to an int.
int const *myVar;               // pointer to a const int.
int * const myVar = ...;        // const pointer to an int.
int const * const myVar = ...;  // const pointer to a const int.

# IO

<< — the ‘put to’ operator. In arg1 << arg2, the << operator takes the second argument and writes it into the first.

1
cout << "Meaning of life: " << 42 << "\n";

>> — the ‘get from’ operator. In arg1 >> arg2, the >> operator gets a value from arg1 and assigns it to arg2.

1
2
int a, b;
cin >> a >> b;

std::endl is a newline that flushes the output buffer, which means it is less performant than "\n".

1
2
3
cout << "Hello" << endl;             // Adds a "\n" and flushes the output buffer.
cout << "Hello" << "\n";             // Adds a "\n".
cout << "Hello" << "\n" << flush;    // Adds a "\n" and flushes the output buffer.

See C++ Standard Library IO for more complex IO operations.

# Arrays

The many ways of initialising arrays:

1
2
3
4
5
6
7
int arr[4];                    // [?, ?, ?, ?] – array is full of garbage values, often zeroes.
int arr[4] = {  };             // [0, 0, 0, 0] – all elements set to 0.
int arr[4] = { 1, 2, 3, 4 };   // [1, 2, 3, 4].
int arr[4] = { 1 };            // [1, 0, 0, 0] – the rest of array is zeroed.

int arr[] = { 1, 2, 3, 4 };    // Array size can be omitted if it can be inferred from RHS.
int arr[] { 1, 2, 3, 4 };      // You can use uniform initialisation instead of copy initialisation.

The size of the array must be able to be determined at compile-time.

# Pointers vs. Arrays

What’s the difference between int* array and int array[]? They both can be used to access a sequence of data and are mostly interchangeable.

The main difference is in runtime allocation and resizing: int* array is far more flexible, allowing allocation/deallocation and resizing during runtime, whereas int array[] cannot be resized after declaration.

In general, prefer using declaring true array-types with [] over pointers-type arrays with *. It’s less error-prone (because you don’t have to worry about dynamic allocation and remembering to free allocated memory) and more readable.

# L-Values and R-Values

An lvalue is a memory location that identifies an object. Variables are lvalues.

In C: an lvalue is an expression that can appear on the LHS or RHS of an assignment.

An rvalue is a value stored at some memory address. Rvalues are different from lvalues in that they cannot have a value assigned to it, which means it can’t ever be on the LHS part of an assignment. Literals are typically rvalues.

In C: an rvalue is an expression that can only appear on the RHS of an assignment.

1
2
3
int i = 10;    // i is an lvalue, 10 is an rvalue.
int j = i * 2  // i * 2 is an rvalue.
2 = i;         // error: expression must be a modifiable lvalue.

Very loosely, Bjarne describes lvalues as “something that can appear on the left-hand side of an assignment” and rvalues as “a value that you can’t assign to.”

# L-Value Reference

An lvalue reference uses a single ampersand &, eg. string& s = "..."

# R-Value Reference

An rvalue reference uses double ampersand &&, eg. string&& s. You’d use this to receive rvalues in functions, like literals and temporary objects. Doing this means you can avoid unnecessarily copying a value that is a ‘throwaway’ on the caller’s side.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Takes in an l-value reference which forces the caller to pass in variables.
void GreetLvalue(string &name) {    
  cout << name << endl;
}

// Takes in an r-value reference which forces the caller to pass in literals 
// or temporary objects.
void GreetRvalue(string &&name) {   
  cout << name << endl;             
}

// Const references let the caller pass both lvalues and rvalues alike.
void Greet(const string &name) {    
  cout << name << endl;  // Note: `const string &` will create a temporary variable behind the
}                        // scenes and then assign it to `name`. This is why you can pass both
                         // lvalues and rvalues to a const l-value reference like this.
int main() {
  string myName = "Tim";
  GreetLvalue(myName);     // ✓
  GreetLvalue("Andrew");   // Error: cannot bind non-const lvalue reference.

  GreetRvalue(myName);     // Error: cannot bind rvalue reference.
  GreetRvalue("Andrew");   // ✓

  Greet(myName);           // ✓
  Greet("Andrew");         // ✓
}

# Modularity

# Separate Compilation

C++ supports separate compilation, where code in one file only sees the declarations for the types and functions it uses, not the implementation. This decouples the smaller units comprising a project and minimises compilation time since each unit can be compiled only if they change.

We take advantage of separate compilation by listing out declarations in a header file. Example: ![[Knowledge/Engineering/Languages/assets/vector-header-cpp.png|500]]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Vector.h — the header file defining the Vector class and its properties and methods (but without implementation)
class Vector {
    public:
        Vector(int size);
        double& operator[[]];
        int size();
    private:
        double* elements;
        int capacity;
};
1
2
3
4
5
6
7
8
// Vector.cpp — the implementation for Vector.h
#include "Vector.h"

// Implementing the constructor and methods outside of the class definition.
Vector::Vector(int s) :elements{new double[s]}, capacity{s} {}   

double& Vector::operator[[]] { return elements[i]; }
int Vector::size() { return capacity; }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// user.cpp — the user of Vector.h, who has know idea about how it's implemented.
//            It only knows about the declarations inside Vector.h

#include <iostream>
#include "Vector.h"

using namespace std;

int main() { 
    Vector v(10);
    cout << "Vector size: " << v.size() << endl;
}

# Namespaces

Namespaces define a scope for a set of names. It’s used to organise your project into logical groups and to prevent name collisions when you’re using libraries, for example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
namespace UNSW {
    class Student {
        public:
            string id;

            Student(string id) {
                this->id = id;
            }
    };
}

int main() {
    UNSW::Student me("z5258971");
    std::cout << me.id << "\n";
}

# Error Handling

C++ provides the familiar try and catch blocks for error handling. Note that when an exception is thrown, the destructor for the object that threw the exception is called, enabling RAII.

1
2
3
4
5
6
7
8
9
try {
        
} catch(out_of_range& err) {

} catch(...) {
    // All exceptions are caught here when you use `...`

    throw;  // Use `throw` on its own to re-throw the exception.
}

# Custom Exceptions

Just inherit from std::exception, implement the const char* what() const throw() method, and a constructor that takes in a string error message.

1
2
3
4
5
6
7
8
class MyException : public std::exception {
public:
  MyException(const string &message) : message_(message) {}
  const char *what() const throw() { return message_.c_str(); }

private:
  string message_;
};

# noexcept

Use noexcept at the end of a function signature to declare that it will never throw an exception. If it does in fact throw an exception, it will just directly std::terminate().

1
void something_bad() noexcept;

Why use it?

# Classes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Human {
public:
    string name;

    static string scientific_name;

    // Default constructor.
    // Having this means that `Human` objects will never be uninitialised.
    Human() { ... }

    // Destructor. Called when an instance goes out of scope or on exception.
    ~Human() { ... }

    Human(int age, string name) { ... }

private:
    int age_;
};
// Non-const static class variables must be initialised outside the class definition.
string Human::scientific_name = "homo sapiens";
int main() {
    // **Allocating the object on the heap**
    Human* me1 = new Human(20, "Tim");
    delete me1;
        // **Allocating the object on the stack** (meaning there's not need to call delete)
        Human me2(20, "Tim");
        Human me3{20, "Tim"};     // An equivalent way of instantiating a class.
        Human me4;                // Implicitly calls the default constructor.
}

# Inheritance

Inheritance is done with the syntax class Foo : public Bar. Also see Protected and Private Inheritance.

# Member Initialiser List and Delegating Constructors

Use a member initialiser list following a constructor signature to initialise class variables and invoke other constructors.

1
2
3
4
5
6
7
8
class Foo {
public:
    int bar;
    string baz;
    Foo() : Foo(0, "") {}
    Foo(int num) : Foo(num, "") {}
    Foo(int num, string str) : bar(num), baz(str) {}
};

# Virtual Methods

Virtual methods are methods that have an implementation but which may be redefined later by a child class.

# Object Slicing

When you copy a child object to a variable of the type of the parent, the members specific to the child are ‘sliced off’ so that the resulting object is a valid instance of the parent type.

1
2
Parent foo = Child();   // When the child object is copied to a variable of a parent type,
                        // all its members and overridden methods are 

Object slicing does not happen in the same way in most other languages, like Java. When you do Parent foo = new Child() in Java, foo’s overriden methods are still intact and will be called instead of the base method. This is because languages like Java use implicit references to manipulate objects and objects are copied by reference by default, unlike C++. (sourced from GeeksForGeeks)

# Instantiating Classes

There are several ways of instantiating a class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void func() {
    // Allocated on the stack:
    Foo f1;              // Implicitly calls the default constructor, Foo().
                         // In other languages like C#, this would be an uninitialised object.
    Foo f2 = Foo(1);     // Copy initialisation.
    Foo f3 = 1;          // Also copy initialisation.
    Foo f4(1);           // Direct initialisation.
    Foo f5{1};           // List initialisation (generally preferred because it avoids implicit type 
                         // conversions and avoids creating unnecessary temporary objects).
    Foo f6 = {1};        // Also copy initialisation, but with an initialiser list.
    Foo f7();            // You'd think this is calling the default constructor, but it's not.
                         // See 'most vexing parse'. Use `Foo f7;` instead to call the default ctor.
    // Manually allocated on the heap (avoid when posssible):
    Foo* f8 = new Foo();
    delete f8;
}

Note that when using brace initialisation, the constructor that takes in a std::initializer_list will be preferred for invocation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Foo {
public:
    Foo(initializer_list<int>) {
        cout << "Initialiser list constructor called.\n";
    }
    Foo(const int&) {
        cout << "Normal constructor called.\n";
    }
};
int main() {
    Foo b(42);  // Normal constructor called.
    Foo c{42};  // Initialiser list constructor called.
    return 0;
}

General Guidelines *:

# Const Objects

const objects prevents its fields being modified. You can only call its const methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Student {
public:
    string name;

    Student() {
        this->name = "Andrew";
    }

    void setName(string name) {
        this->name = name;
    }
};

int main() {
    const Student s;        
    s.name = "Taylor";     // ✘ not fine because this modifies a class variable.
    s.setName("Taylor");   // ✘ not fine because this calls a non-const method.
}

Note the differences between const Foo foo = ...; in C++ vs. const foo = ... in JavaScript/TypeScript. In JavaScript, you can still mutate the fields freely by default, you just can’t reassign foo to point to a different object.

# Final Methods

Postfixing a method signature with the final keyword will make it so that it cannot be overridden by a deriving class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class FooParent {
public:
    virtual void Info() = 0;
};

class Foo : public FooParent {
public:
    void Info() override final {}
};

class FooChild : public Foo {
public:
    void Info() override {}      // Error: cannot override final funtion
};

# Explicit Methods

You can prefix explicit in front of a constructor or method signature to prevent implicit type conversions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class MyVector {
public:
    int size;

    explicit MyVector(int num) : size(num) {}
};

int main() {
    MyVector v = 2;      // Without an explicit constructor, this actually invokes `MyVector(2)`.
}                        // When you define `explicit MyVector(int num)`, this call would cause an error.

# Friend Classes

Inside your class body, you can declare a class as a friend which grants them full access to all your class’ members, including private ones. Just remember the inappropriate but memorable phrase: “friends can access your privates”.

A class can declare who their friends are in their body. Friends are then able to access everything within that class, including private members.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Baby {
public:
    Baby(const string& name) {}

    // Methods of `Mother` will be able to see everything in `Baby`.
    friend class Mother;        

private:
    string name;
};

class Mother {
public:
    Mother(const string& babyName) : baby(babyName) {}

    void rename_baby(const string& new_name) {
        // This is only possible because of `friend class Mother`.
        baby.name = new_name;    
    }

private:
    Baby baby;
};
        int main() {
    Mother mum("Andrew");
    mum.RenameBaby("Andy");
}

When to use friend classes?

Use friend classes sparingly. If you’re in a situation where you want to use a friend class, it’s a red flag, design-wise.

# Deleted Functions

Just like how you can use = 0 to declare a function to be a pure virtual function, you can use = delete to declare a function to be a deleted function, where it can no longer be invoked. This is mostly used to suppress operations and constructors to prevent misuse.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class A {
public:
    A() = default;

    // Deleted copy constructor.
    A(const A& other) = delete;

    // Deleted assignment operator.
    A &operator=(const A& other) = delete;
};

int main() {
    A a1;
    A a2 = a1; // error, copy constructor is deleted.
    a1 = a2;   // error, assignment operator is deleted.

    return 0;
}

# Defaulted Functions

Like deleted functions, you can postfix a constructor or method signature with = default to make the compiler supply a default implementation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class A {
public:
    A() = default;

    // Default copy constructor.
    A(const A &other) = default;

    // Default assignment.
    A &operator=(const A &other) = default;

    // Default destructor.
    ~A() = default; 
};

# RAII

The technique of acquiring resources in the constructor and then freeing them in the destructor is called RAII (Resource Acquisition is Initialisation). The idea is about coupling the use of a resource to the lifetime of an object so that when it goes out of scope, or when it throws an exception, the resources it held are guaranteed to be released. Always design classes with RAII in mind.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class ResourceHolder {
public:
    ResourceHolder() {
        arr = new int[12];
    }
    ~ResourceHolder() {
        delete[] arr;
    }
private:
    int* arr;
};

# Operator Overloading

You can define operations on classes by overloading operators like +, +=, ==, etc.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Coordinate {
public:
  int x;
  int y;

  Coordinate operator+(const Coordinate &other) {
    return {.x = this->x + other.x, .y = this->y + other.y};
  }
};

int main() {
  Coordinate foo{42, 24};
  Coordinate bar{2, 2};
  Coordinate result = foo + bar;
  cout << "Coordinate: " << result.x << ", " << result.y << endl;
  return 0;
}

# Copy Constructor and Operation

A copy constructor is a constructor that takes in a (typically const) reference to an instance of the same class. A copy assignment operator overload also takes in a (typically const) reference to an instance of the same class and returns a reference to an instance of the same class.

1
2
3
4
5
6
7
8
class Foo {
public:
    // Copy constructor.
    Foo(const Foo& other) { ... }

    // Copy assignment operator.
    Foo& operator=(const Foo& other) { ... }  
};

The copy constructor is invoked implicitly in a number of situations:

If you don’t implement the copy constructor yourself, the compiler generates a default copy constructor that performs a simple memberwise copy to form a new object. Often, this default copy constructor is acceptable. For sophisticated concrete types and abstract types, the default implementation should be deleted or manually implemented.

# Move Constructor and Operation

A move constructor is a constructor that takes in an rvalue reference to an instance of the same class. A move assignment operator overload takes in an rvalue reference (to an instance of the same class) and returns a reference to the same class.

1
2
3
4
5
6
7
8
class Foo {
public:
    // Move constructor.
    Foo(Foo&& other) { ... }

    // Move assignment operator.
    Foo& operator=(Foo&& other) { ... }
};

Suppose you have a function that returns a large object (e.g. a big matrix) and you want to avoid copying it to the caller, which would be very wasteful of clock cycles. Since you can’t return a reference to a local variable, and it is a bad idea to resort to the C-style returning of a pointer to a new object that the caller has to memory-manage, the best option is to use a move constructor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Foo {
public:
    Foo(initializer_list<int> vals) {
        arr_ = new int[vals.size()];
        size_ = vals.size();
        int i = 0;
        for (const int& val : vals) arr_[i++] = val;
    }

    // Move constructor.
    Foo(Foo&& other) {
        // Steal the given Foo's members/resources.
        arr_ = other.arr_;
        size_ = other.size_;
        other.arr_ = nullptr;
        other.size_ = 0;
    }

    // Move assignment operator.
    Foo& operator=(Foo&& other) {
        if (this == &other) return *this;

        arr_ = other.arr_;
        size_ = other.size_;
        other.arr_ = nullptr;
        other.size_ = 0;
        return *this;
    }

    void print_vals() {
        cout << "Values: ";
        for (int i = 0; i < size_; ++i) cout << arr_[i] << " ";
        cout << "\n";
    }

private:
    int* arr_;
    int size_;
};

int main() {
    Foo foo{2, 4, 8};
    Foo bar{};

    foo.print_vals();
    bar.print_vals();

    bar = std::move(foo);

    foo.print_vals();
    bar.print_vals();

    return 0;
}

# std::move

Suppose you have a class Foo that implements a move constructor: Foo(Foo&& other). To invoke this, you’d do something like:

1
2
Foo foo;
Foo bar((Foo&&)foo);   // Invoke the move constructor.

Since this is ugly and doesn’t work for auto inferred types, we have std::move instead.

1
2
Foo foo;
Foo bar(std::move(foo));   // Read this like: "moving foo's contents to bar."

Once you’ve used move(foo), you mustn’t use foo again. Because it’s error prone, Bjarne recommends to use it sparingly and only when the performance improvements justify it.

std::move doesn’t actually move anything, which is a bit misleading. It just converts an lvalue to rvalue reference so as to invoke the move constructor. It has zero side effects and behaves very much like a typecast. The actual ‘moving’ itself is done by the move constructor.

A less mislead name for std::move would have been rvalue_cast, according to Bjarne.

std::move(foo) basically says “you are now allowed to steal resources from foo”.

# Templates

Templates in C++ are a code generation system that lets you specify a general implementation of a class or function and then generate specialised instances of that template for different types, on demand.

To make a class or function a template, just prefix the definition with the template keyword followed by a list of type parameters: template <typename A, typename B, ...>.

# Templates vs. Generics

Templates are massively different from generics in other OOP languages like Java.

In Java, generics are mainly syntactic sugar that help programmers avoid boilerplate code. In C++, templates are a code generation system that enables generic programming and more. Some main differences:

Think of C++’s templates as very sophisticated preprocessor commands. It’s used to generate type-safe code. In Java’s generic system, type erasure is used, resulting in a single implementation of the generic class/function rather than many specialised instances of a template like in C++.

# Function Templates

Function templates are a generalised algorithm that the compiler can generate copies of for usage with specific types.

1
2
3
4
template <typename T, typename U>
void foo(T t, U u) {
    // ...
}

A generic swap function:

1
2
3
4
5
6
template <typename T>      // Start of T's scope
void swap(T& a, T& b) { 
    T tmp = a;
    a = b;
    b = tmp;
}                          // End of T's scope

# Class Templates

Basically function templates, but with classes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
template <typename Item>
class Stack {
public:
    void push(Item i);
    void show();
private:
    vector<Item> vec_;
};

// When implementing methods outside of the class, you must fully qualify
// the method name with the prefix `MyContainer<T>::`.
//
// Anything following `::` will be within the class' scope, meaning that
// specifying <T> becomes optional again — because T is known in the class
// body.
template <typename Item>
void Stack<Item>::push(Item i) {
    vec_.push_back(i);
}

template <typename Item>
void Stack<Item>::show() {
    for (const Item& item : vec_)
        cout << item << " ";
    cout << "\n";
}

int main() {
    Stack<int> nums;
    nums.push(42);
    nums.push(24);
    nums.show();

    Stack<string> strs;
    strs.push("Hello");
    strs.push("World");
    strs.show();

    return 0;
}

# Aliases

You can use using to create type aliases for template types.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
template <typename T> 
using MyMap = unordered_map<T, string>;

int main() {
    MyMap<string> map;

    map.insert(make_pair("Hello", "world"));
    map.insert(make_pair("Goodbye", "world"));

    return 0;
}

# Concepts [TODO]

Concepts are predicates that are used to apply constraints to the type arguments you pass to a template function or class. The main reason to use concepts is to get the compiler to be better at preventing misuage.

TODO: notes on concept-based overloading, defining concepts.

1
2
3
4
5
template <...>
    requires

// The equivalent and less verbose way to use concepts without requirements clauses:
template <...>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template <typename T, typename U = T>
concept EqualityComparable =
    requires (T a, U b) {
        { a == b } -> bool;
        { a != b } -> bool;
        { b == a } -> bool;
        { b != a } -> bool;
    }

// ...
static_assert(EqualityComparable<int, double>);
static_assert(EqualityComparable<int>);          
static_assert(EqualityComparable<char>);          
static_assert(EqualityComparable<char, string>);  // Fails.

# Type Traits [TODO]

# Deduction Guides [TODO]

This is for aiding type inference.

# Functors

Functors, or function objects, are instances of a class that implements the function call operator method, operator(), which means that they can invoked as if they were functions themselves. Functors are highly customisable, reusable, stateful functions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class DrinkingLaw {
public:
    DrinkingLaw(int required_age) : required_age_(required_age) {}

    // Implementing this method is what makes this class a functor
    bool operator()(int age) {
        return age >= required_age_;
    }
private:
    int required_age_;
};

int main() {
    DrinkingLaw canDrink(18);
    cout << "I can drink: "
         << (canDrink(21) ? "Yup" : "Nope")
         << endl;
}

# Lambda Functions

You can think of lambda functions as syntactic sugar for inline, anonymous functors.

1
2
3
[_] (params) -> RetType {   // You can omit the return type if it can be inferred.
    // Function body
}

You can use the capture clause (the [] of the expression) to access variables from the outer scope.

A classic use of lambda functions is passing it as the comparator function to std::sort to determine whether one element comes before another.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int main() {
    vector<int> v{2, 4, 8};

    std::sort(v.begin(), v.end(), [] (int a, int b) {
        return a > b;
    });

    for (int val : v) cout << val << " ";
    cout << "\n";

    return 0;
}

# Enums

In addition to structs and classes, you can also use enums to declare new data types. Enums are used to represent small sets of integer values in a readable way. There are two kinds of enums in C++, plain enums and enum classes (which are preferred over plain enums because of their type safety).

# Plain Enum

Declared with just enum. The enum’s values can be implicitly converted to integers

1
2
3
4
5
6
enum Mood { happy, sad, nihilistic };

int main() {
    Mood currMood = Mood::happy;  
    int val = currMood;           // No error, the Mood value is implicitly converted to an integer type.
}

# Enum Class

When you declare an enum with enum class, it is strongly typed such that you won’t be able to assign an enum value to an integer variable or to another enum type. It reduces the number of ‘surprises’ which is why it’s preferred

1
2
3
4
5
6
enum class Mood { happy, sad, nihilistic };

int main() {
    Mood currMood = Mood::happy;  
    int val = currMood;            // Error, Mood::happy is not an int.
}

# Initialiser List

std::initializer_list is a special class whose objects are used to pass around a sequence of data using curly braces {} in a readable, intuitive way.

The compiler automatically converts { ... } to an instantiation of std::initializer_list in these situations:

  1. {} is used to construct a new object: Person person{"Tim", "Zhang"} This will call the constructor of signature Person(std::initializer_list<string> l) {...} if it exists. If such a constructor doesn’t exist, it will look for Person(string firstName, string lastName) { ... }. So basically, it prefers invoking constructors that take in std::initializer_list but it will silently fall back to direct invocation if that fails.
    • Constructors taking only one argument of this type are a special kind of constructor, called initialiser-list constructor.
    1
    2
    3
    4
    5
    6
    7
    
    struct Foo {
        Foo(int,int) { ... };
        Foo(initializer_list<int>) { ... };
    };
    
    Foo foo {10,20};  // Calls the initialiser-list constructor. Calls `Foo(int, int)` if it doesn't exist.
    Foo bar (10,20);  // Calls `Foo(int, int)`.
    
  2. {} is used on the RHS of an assignment: vector<int> vec = { 1, 2, 4 };
  3. {} is bound to auto. E.g.
    1
    2
    
    for (auto i : { 2, 5, 7 })    // `std::initializer_list` is an iterable.
        cout << i << endl;   
    

Interesting questions:

# Iterators

An iterator is an object that points to a specific item in a container. It has methods and operations for iterating over a container.

1
2
3
4
5
6
7
8
std::vector<int> values = {1, 2, 3};

for (std::vector<int>::iterator it = values.begin(); it != values.end(); it++)
    cout << *it << endl;

// Syntactic sugar for the above:
for (int value : values)
    cout << value << endl;

# Iterator Categories

There are different types of iterators in C++, in order to least functionality to richest functionality:

  1. Input iterator — you can only access the container in a single forward pass.
  2. Output iterator — you can only assign values to the container in a single forward pass.
  3. Forward iterator — combines input and output iterators.
  4. Bidirectional iterator — forward iterator that can also go back.
  5. Random access iterator — you can move the iterator anywhere, not just forward and back.

They form a hierarchy where forward iterators contain all the functionality of input and output iterators, bidirectional contains all of forward, and random-access contains all of bidirectional: (sourced from GeeksForGeeks) (sourced from GeeksForGeeks)

The STL containers support different iterator categories: (sourced from GeeksForGeeks)

# Random C++ Features

Other important C++ details.

# Structured Bindings

You can unpack values in C++17, similar to how you destructure objects in JavaScript. It’s just syntactic sugar.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Coordinate {
  int x;
  int y;
};

int main() {
  Coordinate point{2, 4};
  auto [foo, bar] = point;   // foo == 2, bar == 4
  return 0;
}

# Using

There are a few different ways the using keyword is used:

  1. Type aliasing (alternative to C-style typedef). It’s generally more preferred to use using over C-style typedef. It also supports a little more extra functionality that is not available with typedef, specifically for templates. Source
    1
    2
    3
    
    // These two are (mostly) equivalent:
    using Age = unsigned int;
    typedef unsigned int Age;
    
    • typedef is locally scoped. using makes the type alias available in the whole translation unit.
  2. Make an identifier from a namespace available in the current namespace.
    1
    2
    
    using std::cout;
    using std::cin;
    
  3. Make all identifiers from a namespace available in the current namespace.
    1
    
    using namespace std;
    
    • Avoid this as much as possible in large projects. It pollutes your namespace with lots of new identifiers.
    • using namespace should never be used in header files because it forces the consumer of the header file to also bring in all those identifiers into their namespaces
  4. Lifting a parent class’ members into the current scope.
    • Can be used to inherit constructors:
    1
    2
    3
    4
    5
    
    class D : public C {
     public:
      using C::C;  // Inherits all constructors from C.
      void NewMethod();
    };
    

# Copy Elision

By default, when you pass an object to a function, that object is copied over (pass-by-value). When it doesn’t affect program behaviour, the compiler can move the object rather than making a full copy of it. This compiler optimisation can also happen when returning an object, throwing an exception, etc.

1
2
3
4
5
6
7
8
string foo() {
  string str = "Hello, world!";
  return str;  // copy elision occurs here
}

int main() {
  string s = foo();
}

Copy elision is not enforced in the C++ standard, so don’t write code assuming this optimisation will happen.

# Return Type Deduction

In C++14, you can infer the return type of function whose return type is left as auto.

1
auto multiply(int a, int b) { return a * b; }

In A Tour of C++, Bjarne says to not overuse return type deductions.

# Initialiser List

You can make a constructor or method take in an std::initializer_list to let the caller directly pass in a curly brace list like {1, 2, 3, 4} as an argument.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Foo {
public:
  vector<int> values;

  Foo(const std::initializer_list<int>& elems) {
    for (const int &elem : elems)
      values.push_back(elem);
  }
  void foo(const std::initializer_list<int>& elems) {
    for (const int& elem : elems)
        cout << "Elem: " << elem << endl;
  }
};

int main() {
  Foo foo = {1, 2, 4, 8};
  foo.foo({ 1, 2, 3 });
  return 0;
}

When the compiler sees something like {1, 2, 3, 4}, it will convert it to an instance of std::initializer_list (if used in a context like above).

# Casting [TODO]

# Inline Functions

inline functions: when you want a function to be compiled such that the code is put directly where it’s called instead of going through the overhead of entering a new function context, make that function inline.

I.e. if you want the machine code to not have function calls to this function, put inline before the function signature. This tells the compiler to place a copy of the function’s code at each point where the function is called at compile time.

1
inline void foo();

# if-statement With Initialiser

In C++17, you can declare variables inside if statements and follow it up with a condition: if (init; condition) { ... }.

1
2
3
4
5
6
vector<int> vec = { 1, 2, 3 };

if (int size = vec.size())
    cout << "Vector size is not 0" << endl;
if (int size = vec.size(); size > 2)
    cout << "Vector size is > 2" << endl;

# Noexcept Conditions

# Aggregates

Aggregates are either arrays or structs/classes that you didn’t define constructors, private/protected instance variables or virtual methods for. When those conditions are met, that class is an aggregate type and can be initialised with {}.

Even though they both use {}, aggregate initialisation is different from list initialisation!

# PODs

PODs (Plain Old Data) are a kind of aggregate type that do not overload operator=, have no destructor, and there are no non-static members that are: non-POD classes, arrays of non-POD classes, or references. Learn more.

Put simply, PODs are just simple data, or simple data containers, hence the name ‘plain old data’.

# Protected and Private Inheritance

Protected and private inheritance are specific to C++. Other languages usually won’t have this.

Just default to using public inheritance (: public) if you want to represent an is-a relationship. Use protected and private inheritance sparingly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Parent {
public:
    int a;
protected:
    int b;
private:
    int c;
};

class PublicChild : public Parent {
    // a is public.
    // b is protected.
    // c is inaccessible here.
};

class ProtectedChild : protected Parent {
    // a is protected.
    // b is protected.
    // c is inaccessible here.
};

class PrivateChild : private Parent {
    // a is private.
    // b is private.
    // c is inaccessible here.
};

# Class Prototypes

Class prototypes are just like function prototypes. You can declare all your classes upfront and then use them wherever you want throughout the code.

1
2
3
4
5
6
7
// Declare classes.
class A;
class B;

// Define classes later (any order is okay).
class B { ... };
class A { ... };

# User-Defined Literals

It’s possible to define custom literals that improve readability. E.g. <chrono> makes it possible to write: 24h, 42min, etc. directly.

Example:

1
2
3
4
5
6
7
8
constexpr int operator""min(unsigned long long arg) {
    return 60 * arg;
}

int main() {
    cout << "Seconds: " << 42min << "\n";
    return 0;
}

# thread_local

There is a thread_local keyword in C++. When a variable is declared with thread_local, it is brought into existence when the thread starts and deallocated when the thread ends. In that sense, the thread sees that variable as a static variable since it exists throughout its lifetime.

1
thread_local int myInt = ...;

# Unnamed Scopes

Usually, we use {} to define scopes for functions, classes, if-blocks, for-loops, etc., but you can also just use them directly in code to create a restricted scope.

1
2
3
4
5
6
7
void foo() {
    cout << "Hello\n";
    {   // Start of an unnamed scope.
        // A smaller scope containing some statements
    }   
    return;
}

# Union

A union is data structure like a class or struct, except all its members share the same memory address, meaning it can only hold 1 value for one member at a time. The implication is that a union can only hold one value at a time, and its total allocated memory is equal to $\texttt{max(sizeof each member)}$.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
union Numeric {
    short  sVal;
    int    iVal;
    double dVal;
};

int main() {
    cout << "Unions" << endl;

    Numeric num = { 42 };
    cout << num.sVal << endl;    // Prints 42.
    cout << num.iVal << endl;    // Prints 42.
    cout << num.dVal << endl;    // Interprets the bits of 42 using floating point representation (IEEE 754).
}

# Compile-Time If [TODO]

if constexpr() { ... }. C++17.

# Volatile [TODO]

# Extern

From what I understand, extern int foo is basically saying “trust me compiler, there’s an int called foo that is defined somewhere.”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// main.cc
#include <iostream>

int main() {
    // `foo` exists, but its value is defined in some other file. Trust me, compiler.
    extern int foo;

    std::cout << foo << "\n";
    return 0;
}

// foo.cc — there is literally just this one line in this file.
int foo = 42;    

Compiling the above with g++ -o main main.cc foo.cc and it just works.

# Fold Expressions [TODO]

# Appendix:

All the notes under this section are meant to be topics or details you don’t need to care much about to program effectively with C++.

# C++ Compilation [TODO]

Compilation of C++ programs follow 3 steps:

  1. Preprocessing Preprocessor directives like #include, #define, #if, etc. transforms the code before any compilation happens. At the end of this step, a pure C++ file is produced.
  2. Compilation The compiler (eg. g++, the GNU C++ compiler) takes in pure C++ source code and produces an object file. This step doesn’t produce anyting that the user can actually run — it just produces the machine language instructions.
  3. Linking Takes object files and produces a library or executable file that your OS can use.

# Commenting

The advice here is sourced from Google’s C++ style guide.

# File Comments

File comments are preferred but not always necessary. Function and class documentation, on the other hand, must be present with exceptions only for trivial cases.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Copyright 2008, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted
// ... and so on
// 
// Google C++ Testing and Mocking Framework (Google Test)
//
// Sometimes it's desirable to build Google Test by compiling a single file.
// This file serves this purpose.

# Variable Comments

Generally not required if the name is sufficiently descriptive. Often for class variables, more context is needed to explain the purpose of the variable.

1
2
3
4
private:
	 // Used to bounds-check table accesses. -1 means
	 // that we don't yet know how many entries the table has.
	 int num_total_entries_;

# TODO Comments

1
2
3
// TODO(kl@gmail.com): Use a "*" here for concatenation operator.
// TODO(Zeke) change this to use relations.
// TODO(bug 12345): remove the "Last visitors" feature.

# Function Comments

Always write a comment to explain what the function/method accomplishes unless it is trivial. This includes private functions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Returns an iterator for this table, positioned at the first entry
// lexically greater than or equal to `start_word`. If there is no
// such entry, returns a null pointer. The client must not use the
// iterator after the underlying GargantuanTable has been destroyed.
//
// This method is equivalent to:
//    std::unique_ptr<Iterator> iter = table->NewIterator();
//    iter->Seek(start_word);
//    return iter;
std::unique_ptr<Iterator> GetIterator(absl::string_view start_word) const;

# Class Comments

Always write a comment to explain what the class’ purpose is and when to correctly use it. Always do this in the .h file, leaving comments about implementation detail to the implementing .cc file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Iterates over the contents of a GargantuanTable.
// 
// Example:
//    std::unique_ptr<GargantuanTableIterator> iter = table->NewIterator();
//    for (iter->Seek("foo"); !iter->done(); iter->Next()) {
//        process(iter->key(), iter->value());
//    }
class GargantuanTableIterator {
	  ...
};

# Flashcards

Some simple Q-and-A notes to be used as flashcards.

# Questions

Some questions I have that are answered: