Brain Dump

CPP

Tags
language

Is a low level programming language and exists as the mostly backwards-compatible successor to C.

Initially standardised in 1998, c++ has had several revisions. A minor revision in 2003, followed by a large overhaul in 2011 and gradual improvements every 3 years from then on-wards.

Deviations from C

Function Overloading and Name Mangling

The CPP compiler mangles the names of any identifiers and symbols in the produced object files. This is to handle functional overloading, where a function can be declared with different types and the compiler can resolve the correct function depending on the argument types.

#include <iostream>

void foo(void) {
    std::cout << "Nothing" << std::endl;
}

void foo(const char* string) {
    std::cout << "Something: " << string << std::endl;
}

int main() {
    foo();
    foo("Hello");
    return 0;
}
Code Snippet 1: Example of a function with overloaded parameters foo.

In C each symbol in the object file must be uniquely defined. Because CPP originally compiled down to C so it imposes this restriction on the generated object files as well. The way CPP handles this is by having different symbols than what's defined in the source code and then resolves the correct target symbol depending on the provided argument types. The output of the name mangler is deterministic, meaning the same function with the same parameters will always have the same mangled output.

Note: Two functions with the same identifier can also have two different return types. So long as they different in the number of parameters or their types, it's sufficient.

Overloaded Function Pointer Resolution

At compile time C++ must resolve each function call to a function definition. The rules for this grow more unpredictable with overloads and implicit casts. The stages the compiler follows for function resolution are:

  • Filter on name
  • Filter on number of arguments
  • Filter on best match

Resolution fails if no function or more than one function matches the function call.

Table 1: Conversion rules for the best function resolution.
ConversionExampleNotes
Exact Match (no temporary)Adding reference, constPrefers casting T -> T& over T -> const T&
Promotionshort -> float, float -> double
Built-Infloat -> int
User-DefinedSingle argument constructor
User-defined and Built-in4.0 + Number(38)

Declaration, Definition, Initialisation and Assignment

Beyond the classic declaration and definition concepts inherited from C, CPP also divides the assignment step into initialisation and assignment. If you assign a variable at the same time you declare it, you've done an initialisation. If you later reassign that variable to a new value, you've assigned it (not re-initialised it).

void foo() {
    // Declaration, definition and *default* initialisation.
    std::string foo;
    // Declaration, definition and initialisation.
    std::string bar = "";
    // Assignment
    foo = "blarg";
}

Note: This distinction applies even in situations you may not expect. Passing values to a function results in them being copied into a new stack-frame meaning their re-initialised within the context of that function.

Default Parameter Values

C++ introduced default parameter values. You specify the defaults at declaration time (in the header files) and omit the defaults in the definition.

int currentYear();

double contractValue(std::string name,
		     char expiryMonth,
		     int expiryYear = currentYear());
Code Snippet 2: A header file with a function specifying a default value. code:def-param
double contractValue(std::string name, char expiryMonth, int expiryYear) {
    return ...;
}
Code Snippet 3: The implementation for the header defined in code:def-param. Notice the lack of re-specifying the default value for expiryYear.

Language

Basic Types

CPP inherited all of the basic fundamental types from C including ints, floats, characters, pointers and more. There're a few amendments and syntactical improvements to the types offered by CPP.

Digit Separators c14

// You can use ' to separate digits for readability like _ in python.
const int num = 1'000'000;

Binary Literals c14

No longer will creating bit-flags be so cumbersome: 0b101010.

Functions

Inline Functions

CPP inherits the inline function concept from C++ with some slight amendments for classes. Any function that is declared AND defined within the class is implicitly inlined. You can also mark a function defined outside of a class as inline with the inline keyword.

Lambdas c11

Create small anonymous functions that can capture the state of local variables.

template <class T>
T sum_squares(const std::vector<T> &v) {
    T result = 0;
    std::for_each(v.begin(), v.end(),
		  [&result](const T &t) { result += t * t; });
    return result;
}

The contents of the square brackets determines the list of variables from the local scope that are captured by the lambda, forming a closure with them.

Classes

// ClassName.h
class ClassName {
public:
    void method1();
    std::string attr1;

private:
    void privateMethod;
    int attr2;
};

// ClassName.cpp
void ClassName::method1() {
    std::cout << "Method 1" << std::endl;
    // The pointer this references the current class.
    this->privateMethod();
}

void ClassName::privateMethod() {
    std::cout << "privateMethod" << std::endl;
    // We can access instance variables without this->.
    std::cout << "attr1: " << attr1 << std::endl;
    std::cout << "attr2: " << attr2 << std::endl;
}

RAII

A concept at the heart of C++ and OOP in C++. Essentially the acquisition of resources is the initialisation of an object, and the relinquishing of resources is the destruction of that object.

Constructors

Are functions of a class used to initialise a new object for that class.

Note: Providing a constructor with some arguments prevents C++ from auto-generating a default constructor, meaning default initialisation will no longer work.

  • Initialising Data Members

    Data members for a class are initialised by the constructor. The classic assignment = form of initialisation within the body of the constructor doesn't give the class a chance to initialise the data members so instead C++ uses an alternative syntax.

    class Foo {
      public:
        Foo();
    
      private:
        int anInt;
        std::string aString;
    };
    
    Foo::Foo() {
        // Foo.anInt and Foo.aString have already been initialised,
        // we're simply re-assigning them here.
        anInt = 5;
        aString = "Hello";
    }
    
    // This syntax allows us to initialise the members directly.
    Foo::Foo() : anInt(5), aString("Hello") {}
  • Multi Argument Constructors

    C++ is unlike java and many other languages in that declaring a variable is sufficient to initialise that variable (see declaration, definition, initialisation and assignment). In case we don't supply an initial value, most types will be default initialised if they have a default constructor.

    int foo; // Has a default value, probably garbage.
    std::string str; // Default constructor called.
    

    Even the assignment operator when used alongside a declaration actually results in a constructor call with a single argument of the assigned type.

    // The argument here is a cstyle string which the constructor of std::string copies
    // into heap memory. We're not assigning a cstyle string to the type of std::string,
    // we're performing a function call with one argument.
    std::string str = "foo";
    // Under the hood c++ actually does something like this.
    std::string str;
    str.operator=("foo")

    To allow multi-argument constructors, the C++ standard introduced a new syntax for initialising variables. It looks semantically equivalent to a function call with all the required arguments.

    std::string foo("bar");
  • Copy Constructor

    Is a special constructor that the compiler implicitly calls whenever there's an attempt to initialise an object with an existing instance of the same class as that object.

    Foo::Foo(const Foo &target) : d_fooField(target.d_fooField) {}

    Warn: The constructor takes a reference, and not an instance, because otherwise it would copy the argument parameter leading to infinite recursion.

  • Copy Assignment Construction

    Copy assignment construction is a special application of the assignment operator where we try to initialise a new object by copying in the values of an existing object. It's used to allow a class to cleanup old values and copy in new values into an existing object. Essentially an in-place update operator for a class.

    class Foo {
    public:
        Foo& operator=(const Foo &foo);
        // ...
    };
    
    Foo& Foo::operator=(const Foo &foo) {
        if (this != &original) {
        // Copy values from foo into this.
        }
    
        // Must return a reference because return
        // value should be an lvalue that can be
        // chained (i = j = k).
        return *this;
    }
    
    Foo foo; // Default constructor called
    Foo bar(foo); // Copy constructor called
    bar = "bar"; // Copy assignment constructor called
    

    Note: To prevent allowing a class to be copied like this, you can declare a private copy-assignment method and avoid providing a definition. This prevents it from being called.

  • Constructor Delegation

    Allows you to call one constructor from another.

    class Foo {
      public:
        Foo(std::string str) { /* ... */ }
        Foo() : Foo("default string") {}
    };

Initializer Lists c11

Are C++11s solution for the most vexing parse problem.

Their are essentially a drop in replacement for the classic parenthesis based constructor call syntax, with some additional extensions to support uniform initialisations for user-defined collections.

std::vector v1;          // Default initialised
std::vector v2();        // Default initialised
std::vector v3{};        // Default initialised with empty initialiser list
std::vector v4{1,2,3,4}; // Initialised with initialiser list

The semantics of how initializer lists work are relatively simple. If a class defines a constructor taking a single argument of type std::initializer_list<T> then the compiler will construct an initializer_list object and pass it to that constructor. If the class does not have such a constructor then each argument of the initializer list is passed as a regular argument to a constructor taking a matching type using the same lookup rules as a regular constructor call.

Note: The empty initialiser list is an exception. When used the default constructor for the class is invoked, not the constructor taking an initializer_list.

Note: Initializer lists only have const access to items in the argument list.

  • Narrowing Prevention

    Initializer lists also prevent implicit narrowing type conversions.

    int i0 = 66.6; // OK, but loses precision
    int i1{66.6};  // Compile-time error: narrowing
    
  • Syntax Sugar for Argument Lists

    You can use initializer lists as a convenient way to pass a collection of arguments, like you can with variadic parameters.

    int addAll(std::initializer_list<int> nums) {
        int result;
        for (auto num : nums) {
        result += num;
        }
        return result;
    }
    
    addAll({1,2,3,4,5}); //=> 15
    

Operators

Are just regular member functions of a class that provide certain features of that class in relation to other classes.

  • Precedence and Associativity

    Table 2: List of available operators in order of precedence
    OperatorAssociativity
    ::L
    x++, x--, T(), f(), x[], ., ->L
    ++x, --x, +x, -x, !, ~, (T), *x, &x, sizeof, new, deleteR
    .*, ->*L
    *, /, %L
    +, -L
    >>, <<L
    <, <=, >, >=L
    &L
    ^L
    \(\vert\)L
    &&L
    \(\vert\vert\)L
    ?:, throw, =, +=, -=, *=, /=, %=, >>=, <<=, &=, ^=, \(\vert =\)R
    ,L

Inheritance

// Parent class.
class Animal {
  public:
    Animal::Animal(int numberOfLegs);
    int numberOfLegs() const;
    // ...

  private:
    int d_numberOfLegs;
};

Animal::Animal(int numberOfLegs) : d_numberOfLegs(numberOfLegs) {}
int Animal::numberOfLegs() const { return d_numberOfLegs; }

// Create a derived object.
class Cat : public Animal {
  public:
    Cat();

  private:
    int d_numberOfEars();

    friend void swap(Cat &a, Cat &b);
};

// Constructor which calls the parent class constructor.
Cat::Cat() : Animal(4), d_numberOfEars(2);

void swap(Cat &a, Cat &b) {
    // Call parent class swap function for Animal.
    // Note: taking a reference to a parent class doesn't
    // perform a conversion, since the fields are packed
    // the same in both the base and subtype struct.
    swap(static_cast<Animal &>(a), static_cast<Animal &>(b));
    std::swap(a.d_numberOfEars, b.d_numberOfEars);
}
Table 3: Properties of the different kinds of inheritance. The default for struct is public and class is private.
InheritanceAnything that is ? in base classwill become ? in derived class
Publicpublicpublic
Privatepublic or protectedprivate
  • Finalising Implementations

    You can mark a class or a member function as final and any later attempt to override or inherit from it will fail.

    class Foo {
      public:
        virtual void bar() final;
    };
    
    class Bar final : public Foo {
        // Error, cannot override final member function bar().
        // void bar() override {}
    };
    
    // Error, cannot override final class Bar.
    // class Baz : public Bar {};
    

    Note: Final and override are both only keywords after the parens of a function. Outside of this context you can use them as regular identifiers even for variable names. Don't do this, it's confusing.

    See also -Wsuggest-final-types, -Wsuggest-final-methods, and -Wsuggest-override.

  • Method Binding

    Method binding can be either static or dynamic, defaulting to static. With static binding the method to call is determined by the static type of the object we use to call the function. With dynamic binding we store a pointer to a vtable vPtr tied to the objects type in the object itself. The compiler builds the vtable (Virtual *Function Table) which keeps a pointer to a method (either overridden in the derived type or pointing to the method in the parent type) for each virtual method in the base type (and all its parent types).

    Note: The key thing to remember about dynamic binding is that the binding to the method is deferred until runtime.

    Cat cat;
    Animal& animal = cat;
    
    // The type of the variable to the left of a function call
    // determines which function to call, since the function is
    // bound at compile time.
    cat.animalMethod();    // Calls implementation in derived Cat
    animal.animalMethod(); // Calls implementation in base Animal
    
    // We can also explicitly call the implementation in a base class.
    cat.Cat::animalMethod();
    cat.Animal::animalMethod();
    Code Snippet 4: Demonstration of classic static binding.
    class Animal {
      public:
        virtual void sayHello();
        // Note: to force implementing classes to provide an implementation
        // for the virtual function suffix the declaration with ~= 0~.
        // virtual void sayHello() = 0;
    };
    
    class Cat {
      public:
        // Note: the virtual qualifier here is [[https://stackoverflow.com/a/4895297/6247387][optional]], but recommended
        // for clarity.
        // Note: the override keyword simply ensures you're deriving an
        // existing function, and will fail if some parent type doesn't
        // define it.
        // Note: The virtual and override specifiers can only be used
        // within the body of a class. To implement them they should be
        // omitted.
        virtual void sayHello() override;
    };
    
    Cat cat;
    Cat &catRef = cat;
    Animal &animalRef = cat;
    
    catRef.sayHello();    // Calls implementation in cat class.
    animalRef.sayHello(); // Calls implementation in cat class.
    
    Code Snippet 5: Demonstation of dynamic binding.

    Note: Dynamic binding only works with references or pointers.

    Note: You should always declare a destructor as virtual, to ensure its binding is dynamic and the correct method is called at runtime. Although this is a good rule of thumb it's not a rule.

Upcasts and Object Slicing

Casts in C++ differ from the ones in Java in that casting a variable to a new type actually slices the fields of the type. For example casting a Cat to an Animal creates a new animal instance with only the fields in the base type copied over. On the other hand if we assign to a reference of the parent type there's no newly sliced object, instead the reference points to the same object as the original variable but the function can only access the fields of the parent type through that reference.

All of this is only possible because classes are structures and the arrangement of fields don't differ between parent and child types.

Cat myCat;

// Safe implicit casts.
Cat& catRef = myCat;
Cat* catPtr = &myCat;
Animal& animalRef = myCat;
Animal* animalPtr = &myCat;

// Object slicing.
Animal animal(cat);
animal = cat;

Enumeration Classes

Are an extension of the regular numerical enumerations provided by C. They differ in that enumeration classes don't dump the enumeration entries into the global scope. Instead each enumeration entry is placed in a namespace matching the enum class.

enum Color { e_RED, e_BLUE, e_GOLD };
Color color = e_GOLD; // Can assign from global scope.

enum class CColor { e_RED, e_BLUE, e_GOLD };
CColor color = CColor::e_GOLD;

Beyond name-spacing enum classes also allow specifying a base type for the enumeration (such as a character instead of an integer) and they cannot be implicitly casted to integers (to do this you must static_cast).

Reference Qualifiers

Allows overriding member functions based on the temporariness of *this.

For example, if you have a class containing a string and a method to return that string you likely don't want to have to copy and return the value of that string if the object containing the string is about to go out of scope. It makes sense to simply move the string outside of the object.

class StringBuilder {
    std::string d_str;

  public:
    std::string get() const & { return d_str; }
    // When *this is temporary, move d_str outside of *this.
    //
    // A new string will be initialised from the R-value of d_str,
    // and returned. d_str is essentially cheaply moved out of
    // StringBuilder.
    std::string get() && { return std::move(d_str); }
};

StringBuilder sb;
// ...
sb.get(); // Calls overload 1, sb still exists past this point.

StringBuilder{}/* ... */.get(); // Calls overload 2, *this is temporary

The way we attach a reference qualifier is by adding const & or && to after the closing parenthesis of a member function. This isn't limited to just these two. You can overload based on const, volatile, reference, and R-value.

Note: If you attach a reference qualifier to a member function, you can't have an overload with no ref-qualifier.

Automatically Defined Methods

A C++ class has a rich set of methods given to it for free.

Table 4: List of available member functions and what version their available from.
MethodFrom Standard
Default constructorC++3
Default destructorC++3
Copy assignment operatorC++3
Copy constructorC++3
Move constructorC++11
Move assignment operatorC++11

There are some situations where the compiler will delete these default method definition. For example if you define a custom constructor then the default constructor will be deleted.

  • Redefining Implicitly Deleted Member Functions

    class Foo {
        Foo(int n) {}    // Defining this caused Foo() to be deleted.
        Foo() = default; // Re-define the default constructor.
    };
  • Delete a Default Member Function

    By assigning a member-function prototype to delete, you prevent that function from ever being called by any code. Deleted function still participate in name lookup so you can use them as a way to avoid certain casting behaviour during initialisation. For example an short could be casted from an int, but you only ever want your class to be instantiated by shorts directly.

    class Foo {
      public:
        Foo(short n); // Allow creating from a short
    
        Foo(int n) = delete;                     // Inhibit constructing from an int.
        Foo(const Foo& rhs) = delete;            // Inhibit copy constructing Foo objects
        Foo& operator=(const Foo& rhs) = delete; // Inhibit copy-assignment
    };

Exceptions

CPP introduced an exception mechanism. Any function in a program may throw an exception, at which point the call stack of that function will be unwound until a handler for that exception is reached or it terminates the program.

double getPrice(std::string name, char expMonth, int expYear) {
    // ...

    if (!isValid(name)) {
	throw std::invalid_argument(name + " - Invalid name");
    }

    // ...
}

void fn() {
    try {
	double value = contractValue("HG", 'K', 2019);
	// ...
    } catch (std::invalid_argument &ex) {
	// Note: Always catch as a refrence, to avoid object splicing.
	// ...
    } catch (std::string &ex) {
	// A string? You threw a string?
    } catch (...) {
	throw;
    }
}

Note: The exception unwind process cleanly cleans up any local variables. Every object on the stack for a stack frame that's unwound has its destructor invoked. Which is then expected to clean up that objects resources. This is why CPP doesn't have a finally clause. Any cleanup logic should be isolated to the object that manages it.

Warn: By convention if another exception is raised while an exception is propagating (such as in a destructor for an object that was just deleted) then the program immediately terminates. This is because otherwise you'd have two exceptions being raised simultaneously and there's no syntactical way to define this.

Exception Guarantees

The guarantees for a function throwing an exception comes in several tiers:

Table 5: List of several tiers of exception guarantees.
TierDescription
No-ThrowCan never throw – The gold standard
Strong-Exception SafetyIf operation fails, no resource leak, no change in state
Basic-Exception SafetyIf operation fails, no resource leak, possibly a change in state, yet still valid
No exception safetyNo guarantees
  • Idiomatic Safety

    To write a truly exception-safe function (one that doesn't corrupt the local state) functions we follow a simply idiom. Essentially we separate the prepare and modification steps. We build up all the data we need that may throw an exception on the stack and then actually perform destructive operations at the end.

    void safeOperation(X x, Y y) {
        // Preparation
        //   Work done on the stack, or ‘to one side’
        //   Any operation can potentially throw
        //   Any throw abandons the work
        //   The object hasn't been changed
    
        // Completion
        //   Free and reassign resources
        //   Nothing can now throw
    }
    • Copy Constructor example

      A classic example would be an assignment operator. We can create a copy of the target object and then, if that succeeds, swap each field between the original object and the copied object.

      Note: Observe how elegantly this approach would handle destructuring our original objects state. When we create the copy, the copy acquires any resources our new object will need. We then copy any old resources we won't need into our copy, also copying the new resources we will need back into our original object, and then finally we destroy the copy effectively releasing the unnecessary resources all at once in a way that safely maintains the state of the object that the assignment returns.

      Foo& operator=(const Foo &other) {
          // Preparation
          Foo copy(other);
      
          // Completion
          std::swap(d_field1, copy.field1);
          std::swap(d_field2, copy.field2);
      
          return *this;
      }
      // In foo.h
      class Foo {
        private:
          int fooA;
          std::string fooB;
      
          // Let swap access private members
          friend void swap(Foo &a, Foo &b);
      };
      
      void swap(Foo &a, Foo &b);
      
      // In foo.cpp
      Foo &operator=(Foo other) {
          // We pass other by value so the compiler allocates it
          // on the stack for us.
          swap(other, *this);
          return *this;
      }
      
      void swap(Foo &a, Foo &b) {
          std::swap(a.fooA, b.fooA);
          std::swap(a.fooB, b.fooB);
      }
      Code Snippet 6: Copy swap, where preparation is handled in the stack.
    • Destructor No-Throw

      The destructor for an object should have strong no-throw guarantees.

      This is because an objects destructor is always called when it's cleaned up. If a function throws an exception, the stack frame is unwound and any local objects destructors are called to clean them up, and if the destructor now throws an exception then the program has two exceptions thrown at the same stack level and it terminates (this cannot be handled).

Access Specifiers

friend

Allows classes to expose private internal data-members to certain functions or classes.

class Foo {
private:
    int bar;

    friend void useFoo(Foo &foo);
};

void useFoo(Foo &foo) {
    // useFoo can access private members of Foo,
    // because it's a friend.
    std::cout << foo.bar << std::endl;
}

The friend relationship is not transitive. You can friend another class meaning all member functions of that class can access private fields from your class, but if that class friends another class you can't transitively access the original classes private fields through the friended class.

const

Marks a variable or return value or operation as non-modifiable.

Note a function can be marked unique based solely on the value of const. This may surprise you, but recall member functions work by having an implicit pointer to that class (called this) as its first argument. Marking a function as const changes the signature of the function to take a const ClassName *this instead, which is a different type.

Note: Constant variables must be initialised, otherwise there's no way to later give them a value.

A trailing constant on a member function means this function doesn't modify the class object it's called for.

class Foo {
    double foo;

  public:
    double real() const { return foo; }
}

Visibility Specifiers

  • public

    Accessible both within and outside of the object.

  • protected

    Accessible only by the object and derived types.

  • private

    Accessible only by the current type, not derived types.

Memory Management

Storage Duration

Table 6: List of the various storage duration's for different kinds of variables.
TypeDurationLocationDescription
auto variableautostackInitialisation -> End of code block
temporaryautostackInitialisation -> End of full expression
parameterautostackFunction call -> Function exit
static (local)staticdata-segmentInitialisation -> Program exit
instanceobjectobjectObject
static (class)staticdata-segmentProgram start -> Program exit
globalstaticdata-segmentProgram start -> Program exit
dynamicdynamicheapWhen I want -> When I want

Note: A local static variable is initialised the first time that local variable is required (often when the enclosing function is called). This is because the variable may behave differently when called at runtime compared to program startup. Use this when you want to avoid repeatedly allocating (and deallocating) something you intend to reuse.

  • Dynamic Memory Allocation

    The new operator lets us request data from the heap (to instantiate an object), returning a pointer to the new memory, and the delete operator calls the deconstructor of an object and then frees the memory allocated for it.

    X *x1 = new X("aaa");
    
    x1->f();
    
    X *x2 = new X(*x1);
    X *x3 = new X();
    
    *x3 = *x2;
    
    delete x3;
    delete x2;
    delete x1;

    There's a similar syntax for dynamically allocating memory for an array.

    // Initialises 10 ints, calling the default constructor for all of them.
    int *ints = new int[10];
    delete[] ints;
    
    // Can also initialise values like C.
    int *ints2 = new int[5] {1, 2, 3, 4}; // 1, 2, 3, 4, 5, 0
    
    • Placement New

      Allows you to allocate into an existing object reference. Ordinarily new creates memory and calls the constructor. Placement new calls the constructor, but doesn't need to allocate the memory because the memory is passed to it.

      // In this the constructor for X is called twice.
      X x;
      X* p = new (&x) X;
      
      // The addresses are the same because the object was
      // initialised in already allocated memory.
      &x == p;

      If you pass an allocator in place of the address, new will use the allocator to allocate memory and you can later free it using the allocator as well.

      MyAllocator allocator;
      
      std::string foo = new (allocator) std::string("Foo", allocator);
      
      allocator.deleteObject(foo);

L-values and R-values

L-values are values that can appear on the left or right hand side of an = operator. An R-value is a value that can only appear on the right hand side of an = operator.

int x = 100;

// A reference is an L-value and can appear on both the left and
// right hand side of an expression.
int& fn() {
    return x;
}

void usesfn() {
    int a = fn();
    a = 200; // x = 100

    int& b = fn();
    b = 200; // x = 200

    fn() = 300; // x = 300
}
Code Snippet 7: Demonstration of the different use-cases of L-values and R-values.

Note: L-values and R-values can both be bound to constant L-value references.

void foo(int&);       // (1)
void foo(const int&); // (2)

int a = 5;
const int b = 10;

foo(a);   // Calls (1)
foo(500); // Calls (2)
foo(b);   // Calls (2)

Pointers

A pointer is a value that addresses some memory. The semantics of pointers in CPP are identical to C.

  • Null Pointers

    Is a pointer type that does not in-fact point anywhere. Earlier versions of c++ used to implicitly cast 0 to the null pointer, but the need for a strongly typed null pointer value eventually lead to the creation of nullptr and nullptr_t.

    void foo1(Bar *bar);
    foo1(nullptr);
    
    // When you have a function accepting multiple pointer types,
    // nullptr_t is required to be able to handle the nullptr type.
    void foo2(char *bar);
    void foo2(double *bar);
    void foo2(std::nullptr_t bar);
    
    foo2(nullptr);
    foo2((char*)nullptr); // However you can also just manually cast the type here.
    
  • Managed Pointer Types

    Are special pointer types that acquire a resource newly allocated on the heap and automatically free that resource when the pointer goes out of scope.

    template <class T> class SmartPointer {
      private:
        T *myPtr;
    
      public:
        SmartPointer(T *arg = 0) : myPtr(arg) {}
        ~SmartPointer() { delete myPtr; }
    
        T *operator->() const { return myPtr; }
        T &operator*() const { return *myPtr; }
    };
    
    void foo(){
        SmartPointer ptr(new Foo()); // ptr takes ownership of newly allocated heap object
        ptr->doFoo(); // Can use just like a regular pointer type for Foo
        // When ptr goes out of scope the destructor for it will be called and automatically
        // free the allocated memory.
    }
    Code Snippet 8: Demonstration of a safe pointer wrapper type.
    • Unique Pointer

      A unique pointer is a single pointer type which takes unique ownership of an object.

      #include <memory>
      
      std::unique_ptr<Foo> ptr1(new Foo());
      
      ptr1->someMethod(); // Call method on Foo through the pointer type.
      
      ptr1.get(); // Gives access to the raw pointer (for use with legacy libraries).
      
      // You can't copy a unique pointer but you can transfer/steal ownership into a
      // new pointer.
      std::unique_ptr<Foo> ptr2(std::move(ptr1));
      
      ptr2.reset(new Foo()); // Release the old resource and assign a new resource.
                     // This will call delete on the original object because
                     // there's no open pointers to it left.
      ptr2.reset();          // Reset to point to the null pointer (make it empty).
      ptr2.release();        // Stop managing pointer, owner must explicitly delete
                     // it themselves.
      

      Note: unique_pointer accurately handles the situation where we've allocated an array instead of a single object (calls delete[] in its destructor).

    • Shared Pointer and Weak Pointers

      Shared pointers are a pointer that can be shared by multiple other shared pointers and only frees the resource when no shared pointer to it remains. It does this by maintaining a reference count. Every copy operation increases the count and every destructuring operation decreases the count. Shared pointers also accept a destroyer parameter that's used to free the pointer.

      We refer to the memory block where the shared pointer stores both weak and strong reference counts as the control block. The control block is also where the destroyer for the resource will be stored.

      All shared pointers pointing to the same value reuse the same control block. The control block will likely have to be allocated dynamically. To avoid repeat allocations for creating a shared pointer. The standard defines a helper which will allocate both simultaneously.

      std::shared_ptr<X> x1(std::make_shared<X>("a"));

      Note: A shared pointer supports all the same operations as a unique pointer including some more:

      std::shared_ptr<X> x1(new X("a"));
      x1.use_count(); //=> 1 // Strong reference count to object X("a")
      
      std::shared_ptr<X> x2(x1); // Initialise from existing shared pointer
      std::shared_ptr<X> x3 = x2; // Supports assignment initialisation
      x1.use_count(); //=> 3
      
      // When the shared pointer is deleted the reference count drops.
      {
          std::shared_ptr<X> x4(x3);
          x1.use_count(); //=> 4
      }
      x1.use_count(); //=> 3
      

      Note: Shared pointers and weak pointers are designed to be used in thread safe code so all copy and deconstructing operations are atomic. This introduces a notable slowness when passing them by value, so the convention is to pass such pointers by reference (const or otherwise).

      • Weak References

        A weak pointer is a pointer to the same data owned by a shared pointer, except it maintains a different weak reference count. A resource is destroyed once it's strong reference count drops to 0, and a control block is destroyed once its strong and weak reference count is dropped to 0.

        This is to avoid the situation where two objects may have a cyclic reference to each other causing them to keep each other alive even when there's no shared pointer to them anymore; meaning there's no route to them for any code path, but because they point to each other, their reference count never drops and their never destroyed. Consider two objects: A and B. A has a strong reference to B (as a data member) and B has a weak reference to A. Also there's one shared pointer with a strong reference to A.

        Table 7: Demonstration of the state of the weak reference example above.
        ObjectWeak Reference CountStrong Reference Count
        A11
        B01

        What happens when the shared pointer to A is freed?

        1. The strong reference count for A drops to 0, so the resource for A is freed but the control block is not because there's still a weak reference to it.
        2. On free the strong reference from A -> B is freed so the strong reference count for B drops to 0. Because both the strong and weak reference count for B are now 0 the resource and control block for B are freed.
        3. The weak pointer from B -> A is freed so the weak reference count for A drops to 0 and the control block for it is freed.

        Warn: A weak pointer doesn't prevent the pointed to object from being cleaned up. Furthermore you cannot use a weak_ptr like a regular pointer type, because this would cause a race condition. If you need access to the pointed to value you must explicitly receive a shared_ptr to it (or null if it's been deleted). This can be done with the lock method.

        std::shared_ptr<X> sp1(new X);
        std::weak_ptr<X> wp1(sp1); // Created from an existing shared pointer
        
        wp1.expired();   //=> true // Does the pointed to value still exist?
        wp1.use_count(); //=> 1    // Number of weak references to object X()
        
        std::weak_ptr<X> wp2(wp1); // Created from an existing weak_ptr
        wp1.use_count(); //=> 2
        
        // lock() obtains the pointee
        // It may return 0
        wp1.lock()->m(); // Obtain and call a method on a shared pointer
                 // Warn: May throw an error because pointer is null
        
        sp1.reset();   // Explicitly release this shared pointer
        wp1.expired(); //=> true // No strong references left so object was deleted
        
        if (std::shared_ptr<X> = sp1.lock()) {
            // Truthy if the shared pointer was correctly acquired
        }

        Warn: You cannot cast a weak pointer to a bool, like you can a shared pointer.

References

A reference is like a pointer, except it must be initialised to some value and it cannot be made to reference some other memory address.

It may help to think of references as aliases to another variable. Modifying the reference modifies the aliased value.

void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int a = 1;
int b = 2;

swap(a, b); // Implicitly made into a reference

Ideally you should always pass user defined types as references (because this avoid any move logic resulting in function calls that may throw errors). Builtin primitives such as double, int, etc. should be passed by value. References for these types have an implicit overhead whenever their accessed (pointer dereferencing) and it may take up more memory than the argument itself (bytes = 4 bytes = 32 bits, on 64-bit machines pointers take up twice that size).

  • R-value References & Move Constructors

    R-value references provide a way to directly address R-values and avoid the need for implicit copying by the compiler. Instead we can now allow classes to cheaply take ownership of data and state stored in an object about to be freed.

    A common application of this would be a move constructor: A constructor taking a mutable R-value reference to an existing object of the same class. The move constructor will move (take ownership) of data stored in that class from the R-value and place dummy data in the R-value so when its freed the data is persisted in our new object. Take for example a temporary vector initialised and returned from a function by value and used to initialise a new vector in the caller function.

    std::vector<int> getVec();
    
    void foo() {
        std::vector<int> myVec = getVec();
    }

    The move constructor can cheaply take ownership of the existing vectors (returned from getVec()) buffer, and gives it a nullptr buffer, so when the R-value is cleaned up the contents of the vector is persisted in myVec.

    template <typename T>
    std::vector<T>(const std::vector &rhs)
        : d_size{rhs.d_size}, d_buffer{new T[rhs.d_size]} {
        // Copy values from rhs to initialise a new vector.
    }
    
    std::vector<T>(std::vector &&rhs)
        : d_size{rhs.d_size}, d_buffer{rhs.d_buffer} {
        // When rhs is finally cleaned up, it'll have no data it
        // needs to free.
        rhs.d_buffer = nullptr;
    }
    Code Snippet 9: Skeleton implementation of a copy constructor and move constructor.

    Note: A move constructor is not added by default to any class that has a non-default constructor.

    Note: What state the object will be in post-move for the default move-constructor isn't well defined. You should assume you'll never be able to use an object after a move.

    • Forcing Transference of Ownership with std::move

      In some situations you may want to force a transference of ownership from one object to another. This can be done by casting the right hand side of an assignment/initialisation from a L-value to an R-value which the std::move helper function can do.

      std::vector<int> src;
      std::vector<int> dst = std::static_cast<std::vector<int>&&>(src);
      // dst stole ownership of data in src, src is now no-longer valid.
      
      // A slightly cleaner way to do this would be to use std::move, which
      // triggers this conversion to an R-value for us.
      std::vector<int> dst = std::move(src);

      Note: std::move(src) isn't doing any sort of modification on src. It's doing compile time manipulations to cast the value from an L-value to an R-value. Passing the R-value to std::vector<int>::operator=(std::vector<int> &&rhs) is what's actually transferring the ownership of data.

      Explicitly marking an L-value as an R-value can be dangerous. If you don't properly remove any state in the src that's now stored in the dst or if you keep using src when it's no longer considered valid. Consider the following example:

      struct Foo {
          int *a;
          Foo() : a{new int{5}} {}
          // Take ownership of a stored by RHS.
          Foo(Foo&& rhs) : a{std::move(rhs.a)} {}
          // Release the resources we're holding onto
          ~Foo() { if (a != nullptr) { delete a; } }
      };
      
      int main() {
          Foo x{};
          std::cout << (void *)x.a << '\n'; //=> 0x1933010
          {
          Foo y{std::move(x)};
          std::cout << (void *)x.a << '\n'; //=> 0x1933010
          std::cout << (void *)y.a << '\n'; //=> 0x1933010
          // y.a is deleted here, and its the same as x.a
          }
          // x.a still points to the same address as y.a
          std::cout << (void *)x.a << '\n'; //=> 0x1933010
      }
      // x.a is deleted once again leading to a double free error.
      

      The Foo object has a move constructor that takes ownership of a pointer stored in the class, but it doesn't swap out the pointer that it's taken ownership of causing a later double free error. When managing resources like this you should make sure the R-value is in a state such that freeing it won't corrupt the newly constructed Foo object. For convenience the std::exchange helper lets us move ownership of a field from an existing class and assign it to a new value at the same time.

      Foo::Foo(Foo&& rhs) : a{std::exchange(rhs.a, nullptr)} {}

      Note: Try not to confuse exchange and swap. Swap only accepts L-values for both left and right hand sides. Exchange uses perfect forwarding and can swap the value with both an L-value or an R-value. It also returns an R-value reference to the original value so you can assign from it. Swap returns nothing.

Type Conversions

C++ supports both explicit and implicit conversions for both built in and user defined types. An implicit conversion is one that occurs without the user having to write any code to make it happen. An example could be casting a numerical type to another numerical type with a greater range (a short -> long, or unsigned int -> unsigned long). In these cases the value of the original variable is not lost, because we're casting to a type that can represent more values. In actuality C++ is much more liberal than just this sort of implicit conversion. An

Note: An enum can implicitly be converted to any integer types larger than or equal to an int (and optionally unsigned) so long as the destination type is large enough to hold all of the enumerator values.

Note: A char is the smallest integral type and can be implicitly converted to any other numeric type including floating point ones.

Note: A bool can be converted to and from any numerical type (a 0 value is false, and a 1 value is true). You can also convert pointer types to bools, with the bool being false if the pointer is equal to the nullptr.

Implicit Casts

If a class has a constructor taking only a single argument, you can implicitly convert to an instance of that type to an instance of the class.

class Foo {
  public:
    Foo(const std::string str);
};

void useFoo(Foo foo);
void useFooRef(Foo &foo);
void useFooRefConst(const Foo &foo);

std::string myStr = "Hello world";
Foo myFoo = myStr; // Implicitly converted

// Demonstration of implicit conversion in practice.
useFoo(myFoo); // Copied, but no conversion needed.
useFoo(myStr); // Converted std::string → Foo and then copy.

useFooRef(myFoo); // Pass by reference, no conversion and no copy.
useFooRef(myStr); // *error*: no conversion to modifiable L-value.

useFooRefConst(myFoo); // No copy, trivial conversion (since myFoo is
		       // modifiable, and useFooRefConst wants a read
		       // only reference to it).
useFooRefConst(myStr); // Conversion std::string → Foo and reference to const
Code Snippet 10: Example of the different situations in which an implicit conversion takes place. code:imp-conv-udt

Note: You may wonder why useFooRef(myStr) throws an error whereas useFooRefConst(myStr) does not, in code:imp-conv-udt. This is because in the former case the compiler needs to convert myStr into a Foo and it does this by creating a temporary object. The temporary object is an R-value, we can't guarantee that it's location can be determined AND is modifiable, meaning we can't pass it as a mutable reference to useFooRef. However in the latter case we only need a constant reference, which can be received through an implicit conversion.

Note: As for why you can only have constant references to temporary variables, it's because if they're mutable the function would be indicating it wants to modify that object and modifying an object that will be disposed of almost immediately is meaningless (and likely an error).

std::string foo("BBG00F0S24M9");

Foo fg(chars);   // No Conversion (Direct Initialization)
Foo fg2 = chars; // Implicit Conversion (Copy Initialization)
fg = chars;      // Implicit Conversion
Code Snippet 11: Demonstration of the difference between an implicit conversion and copy initialisation.

Note: If you don't want to allow implicit casts for a user defined type, you must mark the constructor as for explicit casts only.

class Foo {
  public:
    explicit Foo(const std::string &str);
};

Explicit Casts

C++ inherited the classic (type)obj explicit casting style from C and also introduced a new function-call type(obj) style and a myriad of compile time and runtime casting functions.

Table 8: List of recommended helper functions to perform explicit casts. tbl:expl-cast-generic
HelperExamplePropertiesDescription
static caststatic_cast<NewType>(obj);Fast, cannot weaken const/volatile-qualifiersPerforms implicit or explicit conversions smartly
constant castconst_cast<SameType>(obj);Fast, compile-time, cannot change typeRemove const/volatile qualifiers from a type (avoid!)
dynamic castdynamic_cast<*NewType>(ptr);Run time type cast, null if invalid castCast pointers between polymorphic (class) types
reinterpret castreinterpret_cast<NewType>(obj);The dark arts, probably best not to, eh?Cast from anything to anything (Never Use!!!)

Note: static_cast only checks whether there's a route from the type of obj to NewType. This could be both up and down the inheritance hierarchy. dynamic_cast allows casts between types (throwing a std::bad_cast if the cast is an invalid reference cast (since references must be initialised)).

class Baz; // Derives from Foo, Bar

Baz baz;

Foo* foo_p = &cf;
Bar* bar_p = &cf;

// Allowed but dangerous. if pointer doesn't point to a Baz type
// you may end up with a Baz* to something that isn't a Pointer.
ASSERT_EQ(&cf, static_cast<Baz*>(s_p));
ASSERT_EQ(&cf, static_cast<Baz*>(j_p));

// Dynamic casts are a generally safer alternative. Will return a
// nullptr if the cast fails.
ASSERT_EQ(&cf, dynamic_cast<Baz*>(s_p));
ASSERT_EQ(&cf, dynamic_cast<Baz*>(j_p));

// Can also side cast to sibling parent types.
ASSERT_EQ(s_p, dynamic_cast<Foo*>(j_p));
ASSERT_EQ(j_p, dynamic_cast<Bar*>(s_p));

Note: C-style and functional casts described above actually run the explicit cast functions (see tbl:expl-cast-generic) in the following order:

  • const_cast
  • static_cast (though ignoring access restrictions)
  • static_cast (see above), then const_cast
  • reinterpret_cast
  • reinterpret_cast, then const_cast

User Defined Cast Functions

You can create an operator for your classes that C++ will use when there's an attempt to cast objects of your type to another type.

class Foo {
  public:
    Foo(const std::string &chars);
    operator const std::string &() const;
};

// Cast function for Foo → std::string
// Note this cannot be made explicit in C++03
Foo::operator const std::string &() const { return "Foo"; }

// Application of both implicit and explicit casts.
Foo foo("Hello Friend");
const std::string& s1 = foo;
const std::string& s2 = static_cast<const std::string&>(foo);

Namespaces

Allow grouping names within a namespace to avoid name collisions.

// In bar.h
namespace Foo {
class Bar {
public:
  void bar();

  // Classes also count as namespaces, meaning you can
  // define a class within a class. Note: nested types
  // have access to private members of the enclosing
  // class.
  class Baz {
    void baz();
  };

  // Baz is in the current namespace so we can qualify
  // it directly.
  Baz &baz;
};
} // namespace Foo

// In bar.cpp
namespace Foo {
void Bar::bar() { std::cout << "In bar" << std::endl; }

// The :: (namespace) operator lets us access namespace members.
void Bar::Baz::baz() { std::cout << "In baz" << std::endl; }
} // namespace Foo
namespace Foo {
namespace Bar {
void Baz::fn() { std::cout << x << std::endl; }
} // namespace Bar
} // namespace Foo
Code Snippet 12: Demonstration of a function with unqualified lookups. code:unq-look

Observe the code in code:unq-look. Where is x defined? In CPP the compiler looks in the current namespace, then the parent namespace, etc., etc., until it finds the desired identifier. This applies even for partially qualified variables such as X::Y::Z. To qualify a name (from the global namespace) prefix it with ::, for example ::Foo::Bar::Baz. There's also special logic for class data-members. If the data member x is inherited from a base class then the compiler looks for a definition in each parent class in the order of inheritance (in case of multiple inheritance) until a valid name is found.

Note: Overloading is still isolated by namespace scope. For example consider code:inh-scope. Which function will be called by Derived d; d.fn(5);? The answer is the implementation in derived. The compiler when choosing a function works from the current classes scope upwards. If a definition is found in the current class, the parent class isn't checked (even if it may have a better best function for our argument types). Essentially each subclass introduces a sub-namespace with a higher precedence then the parent scope. This can be fixed by using the definitions from the base-class as shown in code:inh-scope.

class Base {
public:
  std::string fn(int);
  std::string fn(double);
};

class Derived : public Base {
public:
  std::string fn(float);
  // We can fix this by reimporting the ~fn~ name from the
  // base class.
  // using Base::fn;
};
Code Snippet 13: Example of a confusing application of overloading. code:inh-scope.

Using

// anothername is an alias for trncpp in this scope.
namespace anothername = trncpp;
anothername::foo == trncpp::foo;
Code Snippet 14: Namespace aliases.

The using keyword allows us to import a value or sub-namespace from a namespace into the current scope.

using Foo::Bar;
Bar &bar1;
Foo::Bar &bar2;
// We can also import all the entries in a namespace.
using namespace Foo;

Note: the using statement isn't a classical import statement. It doesn't copy definitions from an outer scope into a local scope, instead it exposes values from a given namespace into the tightest scope shared by the current scope and the target scope. This is most clearly demonstrated by code:using-namespace.

X a;
namespace N1 {
    X c;
    namespace N2 {
	namespace N3 {
	    X a, b, c;
	}
    }
    namespace N4 {
	X a;
	void locationTest() {
	    // Imports a, b, c into the shared namespace N1,
	    // for the duration of the current scope.
	    using namespace N2::N3;

	    a; // From N4
	    b; // From N3
	    // c; // Ambiguous, conflicts with definition in N1 and N2::N3.
	}
    }
}
Code Snippet 15: . code:using-namespace
  • Using For Type Aliases

    using within a namespace exposes a variable from that namespace.

    namespace foo {
        class Foo;
    }
    
    namespace bar {
        using foo::Foo;
    }
    
    bar::Foo bar; // Can access class in foo namespace from bar.
    

    Note: This introduces a slight dilemma. If you want to use using to shorten the definition of a type, you expose that type through that namespace. This would be fine in a .cpp file which is compiled in a single text-unit, but in a header file that definition would be exposed in every file that includes it. So avoid using unless absolutely necessary, and definitely do not use it in a .h file.

Unnamed Namespaces

Are an alternative to the static modifier from C. Names declared within an anonymous namespace are injected into the containing namespace but in a way that is not exposed outside of the current translation unit. Such namespaces differ from static in that they have external linkage and a few other CPP specific advantages. At compile time the compiler will generate unique names for each identifier.

Names in the unnamed namespace are available in the containing namespace for that translation unit only.

// in foo.h
void printBar();

// in foo.cpp
#include <iostream>

namespace {
    int printFoo() {
	std::cout << "Hello world" << std::endl;
    }
}

void printBar() {
    printFoo();
}

// in main.cpp
#include <foo.h>

printFoo(); // error: ‘printFoo’ was not declared in this scope
printBar(); //=> Hello world

Argument Dependent Lookup

Is an aspect of CPP where the compiler will first look in the namespace of each of a function calls argument types for an definition of the function. It does this after failing to find a definition within the current functions scope/body (can be given with a using statement), but before it does an unqualified lookup in the namespace of the current function itself.

#include <iostream>

namespace foo {
    class Foo {};

    void swap(Foo foo, Foo other) {
	std::cout << "Swap in foo" << std::endl;
    }
}

int main(){
    foo::Foo foo1, foo2;
    // Finds definition in foo namespace because
    // first argument type is in that namespace.
    swap(foo1, foo2); //=> Swap in foo
}

Templates

Are a way for CPP to define things in a generic way.

Templates are, by there namesake, a template of an algorithm or class that is instantiated in each translation unit, the first time the argument types are known, into a specialised implementation. The compiler then checks the sanity of the code (for example a parameter of a generic type might use a certain operator in the function, but that's not defined). Once the program is linked the redundant duplicate definitions are removed and only a single implementation is left.

template<class T>
bool allDiffer(T a, T b, T c) {
    return !(a == b || b == c || a == c);
}

allDiffer<>(1, 2, 3); // With type deduction.
// bool allDiffer(int a, int b, int c) {
//     return !(a == b || b == c || a == c);
// }

allDiffer<double>(1.0, 2.0, 3.0);
// bool allDiffer(double a, double b, double c) {
//     return !(a == b || b == c || a == c);
// }

Note: Until C++17 type parameters cannot be deduced. You can't pass a List<int> to something expecting a List<double>, even though we can cast an int to double without any loss of information.

Warn: Because a template isn't a definition, just a prototype used to instantiate a real function, it's definition must be available at compile time for any function that needs it. Because of this any template implementations must be placed in header files instead of CPP files.

Type Deduction

Inclusion of angle brackets is what determines whether an argument is looked for as a template or a concrete implementation. Omitting the brackets makes the compiler look for both template and non-template definitions (with preference going for the non-template).

template<class T>
bool allDiffer<T>(T a, T b, T c) {
    return !(a == b || b == c || a == c);
}

// A specialisation for integers.
template<>
bool allDiffer<int>(int a, int b, int c) {
    return false; // never true when type = double.
}

allDiffer(1, 2, 3);       // Concrete implementation => allDiffer<int>    => false
allDiffer(1.0, 2.0, 3.0); // Template instantiatied  => allDiffer<double> => true
allDiffer(1, 2.5, 3);     // error: failed to resolve target function
template <class T> class Foo {
public:
    Foo(T value) : d_value(value) {}

    // We can omit the template paramter T from Foo<T>
    // because within the scope of the class T is already
    // intantiated.
    T addTo(const Foo &myFoo);

private:
    T d_value;
};

// Same story here Foo<T> introduce T to the parameter namespace.
template<class T>
T Foo<T>::addTo(const Foo &myFoo) { return d_value + myFoo.d_value; }

Foo<int> foo(5), bar(5);
foo.addTo(bar); //=> 10
  • SFINAE

    A standard idiom regarding template substitution for C++, if a given type cannot be substituted into a template definition, then that is not an error. Only when there's no overload of a template type that satisfies the provided type do we treat it as an error. The goal here being: if we can arrange for invalid code to be generated, we can control which overload the compiler picks.

    This is often used with std::enable_if to restrict certain overloads based on template resolution.

Class Templates

We can also create class templates. The template variable T is accessible anywhere in the definition of the class.

template <class T> class LinkedList {
  public:
    LinkedList(const T &item, LinkedList<T> *previous, LinkedList<T> *next);
    T &item();
    const T &item() const;
    LinkedList<T> *previous() const;
    LinkedList<T> *next() const;

  private:
    T d_item;
    LinkedList<T> *d_previous;
    LinkedList<T> *d_next;
};

// Example generic constructor implementation.
template <class T>
LinkedList(const T &item, LinkedList<T> *previous, LinkedList<T> *next)
    : d_item(item), d_previous(previous), d_next(next) {}

Note: The CPP compiler only instantiates a template member-function if it is used, not when a generic type is declared. For example consider code:temp-inst-type-err. If we never call foo2.timesByFive then the program will compile fine, but if we do then an error will be thrown. If a template function isn't called by the program, then it isn't needed and won't be implemented.

template <class T>
class Foo {
  public:
    Foo(const T &t);

    // Expects T to be an integer type.
    int timesByFive() { return t * 5; }

  private:
    T d_myT;
};

template <class T>
Foo<T>::Foo(const T &t)
: d_myT(t) {}

Foo<int> foo1(5);
foo1.timesByFive(); // Okay, int * 5 is valid.

Foo<std::string> foo1("hello");
foo2.timesByFive(); // Compiler error, cannot times string by 5.
Code Snippet 16: Demonstration of template instantiation where member function may throw an error depending on template type. code:temp-inst-type-err

Typed and Non-Typed Parameters

Templates can accept an arbitrary number of template arguments and even non-type parameters. Non-type parameters are literal values which the template can be given at compile time; offering a chance at early optimisation.

The very clear downside to this sort of approach is that every function call to the template with differing non-typed arguments leads to more code generation.

template <class T>
T accumulate(const LinkedList<T> &data, size_t n) {
    T result = T();
    int count = 0;
    const LinkedList<T> *pos = &data;
    while (pos && count++ < n) {
	result += pos->item();
	pos = pos->next();
    }
    return result;
}
Code Snippet 17: Example of an accumulator taking a runtime value for size. code:argument-templ-param
template <size_t N, class T>
T accumulate(const LinkedList<T> &data) {
    T result = T();
    int count = 0;
    const LinkedList<T> *pos = &data;
    while (pos && count++ < N) {
	result += pos->item();
	pos = pos->next();
    }
    return result;
}
Code Snippet 18: Example of an accumulator taking a compile time template value for size. code:non-typed-templ-param
LinkedList<int> nodeA(5, 0, 0);
LinkedList<int> nodeB(6, &nodeA, 0);
LinkedList<int> nodeC(7, &nodeB, 0);

// Using a regular parameter, and only a single template
// parameter T. Here the template is instantiated once,
// for the first call and then reused for the subsequent
// 4 calls.
ASSERT_EQ(0,  accumulate(nodeA, 0));
ASSERT_EQ(5,  accumulate(nodeA, 1));
ASSERT_EQ(11, accumulate(nodeA, 2));
ASSERT_EQ(18, accumulate(nodeA, 3));

// Using a non-type parameter.
// Here each call instantiates a new template leading to
// even more code generation.
ASSERT_EQ(0,  accumulate<0>(nodeA));
ASSERT_EQ(5,  accumulate<1>(nodeA));
ASSERT_EQ(11, accumulate<2>(nodeA));
ASSERT_EQ(18, accumulate<3>(nodeA));

// Observe how both template definitions can exist side-by-side.
// If we supply the parameters in the right way, the right one
// will be resolved and the code will have the intended effect.
Code Snippet 19: Comparison of the difference between code:argument-templ-param and code:non-typed-templ-param.

Default Parameters

Template parameters, just like regular parameters, can be assigned a default value/type, allowing the caller to omit specifying it.

template <int N, class T, class A = T>
A accumulate(const LinkedList<T> &data) {
    A result = A();
    int count = 0;
    const LinkedList<T> *pos    = &data;
    while (pos && count++ < N) {
	result += static_cast<A>(pos->item());
	pos = pos->next();
    }
    return result;
}

LinkedList<double> nodeA(5.7, 0, 0);
LinkedList<double> nodeB(6.7, &nodeA, 0);
LinkedList<double> nodeC(7.6, &nodeB, 0);

int a1 = accumulate<3, double, int>(nodeA); // T is double, A is int
double a2 = accumulate<3>(nodeA);           // T is deduced as double, A defaults to T
Code Snippet 20: The return value defaults to type T but can be manually overriden.

Template Templates

Is the idea of passing a template parameter which may also be template parameterised in terms of one or more other types.

// S is a template taking a single template parameter which isn't
// named when declared because it has no bearing on type deduction.
template <int N, class T, template <class> class S, class A = T>
// template <int N, class T, template <class X> class S<X>, class A = T>
A accumulate(const S<T> &data) {
    A result = A();
    int count = 0;
    const S<T> *pos = &data;
    while (pos && count++ < N) {
	result += static_cast<A>(pos->item());
	pos = pos->next();
    }
    return result;
}
Code Snippet 21: code:temp-temp

The form shown in code:temp-temp is usable, but requires some very ugly template instantiations: accumulate<3, LinkedList<int>, <int>>(...). The code in code:temp-temp-ns shows an alternative approach which is the more standard approach in C++03. It works by saving the template parameter T in the namespace of the template class S so it's accessible at compile time. This is used for example to reference the key and value types in std::map.

template<class T>
class Foo {
public:
    typedef T foo_type; // Make the type accessible in the class namespace.
    // ...
};

template <int N, class T, class S, class A = T>
typename S::foo_type accumulate(const S &data) {
    // Use typename to access the type within the namespace.
    typedef typename S::foo_type A;
    A result = A();
    int count = 0;
    const S<T> *pos = &data;
    while (pos && count++ < N) {
	result += static_cast<A>(pos->item());
	pos = pos->next();
    }
    return result;
}
Code Snippet 22: code:temp-temp-ns

Template Specialisation

Is a form of template overloading where you can define the same template more than once, but provide specific types for the template type parameters. In this case when a developer tries to instantiate a template with the specialised types the specialisation class is invoked instead of the original template class.

Note: A specialised template has no relation to its original type beyond an identifier. It can have a different size, members, functions, etc.

template <class T, class U, class V>
class Foo {
  public:
    T a_t;
    U a_u;
    V a_v;
};

// A specialisation of the Foo class.
// You can tell beause the class name Foo is followed by <>.
template <>
class Foo<bool, bool, bool> {
  public:
    bool b_t;
    bool b_u;
    bool b_v;
};

Foo<int,bool,float> generic;
Foo<bool,bool,bool> special;

generic.a_t;
generic.b_t; // error: Foo<int,bool,float> has no member b_t
special.a_t; // error: Foo<bool,bool,bool> has no mebmer a_t
special.b_t;

Note: We refer to Full Specialisation as when we specialise all the template parameters of a type leaving template<> before the declaration.

Type Traits

Are a way to expose metadata related to a type, including user-defined types, using specialisation. This allows user-defined types to be substituted into generic functions that call these traits.

template <class T>
bool isMax(const T& t) {
    return t == std::numeric_limits<T>::max();
}
Code Snippet 23: An example of a template function using a trait. code:trait-user

Take for example code:trait-user. If we create a user-defined number type (example BigInt) then we can add an overload for std::numeric_limits<BigInt> and then the existing isMax<T> algorithm will work with our user defined type.

enum Move { e_LEFT, e_RIGHT };

template <class T>
struct DefaultDirectionFor {
    // The default direction for any type T
    static const Move direction = e_LEFT;
};

template <>
struct DefaultDirectionFor<Cat> {
    // Cats move to the right, not the left silly.
    static const Move direction = e_RIGHT;
};

// Can check at compile time which direction we want
// for a given template type.
template<class T>
void doMove(T& obj) {
    switch (DefaultDirectionFor<T>::direction) {
	// ...
    }
}

Forwarding References

A forwarding-reference is a reference type in template types that captures all the details of a type T including CV-qualifiers and L-value or R-value semantics. These are useful for template functions which take either a const T& or a T&& and have essentially the same body. Instead of creating \( \mathcal{O}(n^2) \) overloads you can just write a single template function.

Within the context of a template function taking a forward reference, the parameter for the forward reference remembers the argument may be an R-value but in regular usage it's treated as a L-value (because it has a name). To propagate the argument to other functions taking R-values you must forward the L-value parameter as a R-value.

#include <utility>

template<typename T>
void setName(T&& name) {
    // std::forward creates a copy of the argument if it's an L-value and
    // moves ownership of the argument if its an R-value. Otherwise it's
    // treated as if it's an L-value.
    d_name = std::forward<T>(name);
}

// Equivalent to independently defining the following, all with basically
// the same body. Notice how this would grow as the number of argument types
// also grows.
// void setName(const std::string& name);
// void setName(std::string&& name);

setName("foo"); // Calls an R-value overload of setName

std::string str{"bar"};
setName("bar"); // Calls an L-value overload of setName
Code Snippet 24: Possible implementation of a setter method taking a forward-reference of type T. Note: In template contexts T&& if a forwarding reference, not a R-value of type T. code:perf-forward
Table 9: Reference collapsing rules for std::forward. These allow perfect forwarding to work. These rules apply because you cannot have a reference to a reference, therefore we collapse such references to regular reference types.
What you want foo(?)What you pass std::forward<?>()What you get
T&&T&
T&&&T&
T&&&T&
T&&&&T&&

Note: The general recommendation is to use perfect forwarding for library code because users can pass any argument types to the library. Note You should try to constrain expected types where appropriate. The generic implementation in code:perf-forward will accept any type for the argument where you may only want T to be a string-type.

Warn: Be cautious about the application of forward references. Only where T is being resolved can the type be a forward-reference. If T was resolved earlier it's a R-value reference.

// A forward-reference.
struct foo {
    template<typename T>
    void foo(T&& t);
};

// A R-value reference.
template<typename T>
struct foo {
    void foo(T&& t);
};

// A R-value reference.
template<typename T>
struct foo {
    template<typename U>
    void foo(T&& t);
};

Variadic Templates

Templates taking an arbitrary number of T types. This works recursively using a declaration called parameter packs. A parameter pack is a template type declared as typename... Ts.

template <typename... Ts>
struct Foo {};

Foo<> f0;
Foo<int> f1;
Foo<int, double> f2;
Foo<int, double, Foo<std::string>> f3;

Parameter packs are processed recursively with the template processing the head of the pack and then recursively processing the rest of it.

// Declare a base case, for when there's only one type.
template <typename T>
T sub(T head) {
    return head;
}

// Declare a recursive case for >1 template types.
template <typename T, typename... Ts>
T sub(T head, Ts... tail) {
    return head - sub(tail...);
}

auto n = add(10, 3, 2, 1); //=> 10-3-2-1

The expansion of a parameter pack (such as in sub(T head, Ts... tail)) supports pattern expansion. Anything placed before the expansion is expanded for each element of it.

template <typename... Ts>
void printAddresses(Ts... xs) {
    variadicPrint(&xs...);
}

printAddresses(5, 10.5); // => variadicPrint(int&, double&)

You can determine the length of a parameter pack with sizeof...(Ts). If a template takes multiple parameter packs they must have the same length.

Compile Time Expressions

Constant Expressions

Allow us to evaluate certain expressions at compile time. This could be useful for example to have an array whose size is defined by the result of a function call that can be calculated at compile time.

constexpr size_t MBtoB(int n) {
    return n * 1024 * 1024;
}

int main() {
    constexpr int numberOfMB = 1;
    char nums[MBtoB(numberOfMB)];
    std::cout << sizeof(nums) << std::endl; //=> 1048576
}

The semantics for when a constexpr is evaluated aren't guaranteed to be at compile time unless the expression it's used in is also a constexpr. For example std::cout << MBtoB(10) may evaluate MBtoB at compile time or at run-time. If the expression contains a non-constexpr parameter then it can only ever happen at run-time: int i = getNum(); MBtoB(i);.

Static Assertions

Allow us to trigger a failure at compile time based on some constant-expression assertion. This is often useful in template programs to restrict the allowed subset of template parameters based on some traits.

class MyClass;

template <typename T>
struct is_cool {
    static constexpr bool value = false;
};

template <>
struct is_cool<MyClass> {
    static constexpr bool value = true;
};

template<typename T>
void doSomethingCool() {
    static_assert(is_cool<T>::value, "Type T isn't cool enough for this");
}

Type Inference

The auto keyword is a placeholder for whatever values satisfies the right hand side of an expression.

int        i   = 0;
int &      ir  = i;
const int  ci  = 0;
const int &cir = 0;

// Auto deduces by value type, so lifetime modifiers are discarded
// when copying by value.
auto ai   = i;   //=> int
auto air  = ir;  //=> int
auto aci  = ci;  //=> int
auto acir = cir; //=> int

// You can attach a reference modifier to the auto declaration to inherit
// reference modifiers. This is needed otherwise you'd discard them.
auto &ri   = i;   //=> int&
auto &rir  = ir;  //=> int&
auto &rci  = ci;  //=> const int&
auto &rcir = cir; //=> const int&

// Any other specifiers are used with the actual type substituted as you'd
// expect.
const auto ci   = i;   //=> const int
const auto cir  = ir;  //=> const int
const auto cci  = ci;  //=> const int
const auto ccir = cir; //=> const int

// Pointers are a special case where the pointer asterisk is optional.
// You can include it for readability.
auto   ip   = &i;  // => int*
auto*  ip2  = &i;  // => int*
auto   ipp  = &ip; // => int**
auto** ipp2 = &ip; // => int**
Code Snippet 25: Demonstrations of the different type inferences of the auto keyword.

Declaring Expression Types

One downside of auto is that it may discard qualifiers such as references. If you try to assign the result of an expression that is a reference, auto will copy that reference by value and store it as the non-reference type.

For example:

int& getResult();

void foo() {
    auto bar = getResult();
    // What's the typeof bar? It's int.
    // We copied the value returned by getResult (which is a reference)
    // into a new variable of type int, where we intended to store an
    // int&.
}
Code Snippet 26: code:auto-ref-loss

For situations like this you can use decltype(expr) to reference the true result type of expr.

decltype(getResult()) bar = getResult();
// Or even better, avoiding the code repetition.
decltype(auto) baz = getResult();
Code Snippet 27: Variant of code:auto-ref-loss with reference semantics being maintained.

Note: If you're curious why auto doesn't do what decltype does already, it most likely is for simplicity reasons. Implementing auto was been easier than implementing decltype so auto came first and decltype later and now the two work cooperatively together.

std::vector<Person> persons;

// To create a collection of iterators over persons.
typedef std::vector<Person>::iterator PersonIterator;
std::vector<PersonIterator> iterators;

// Equivalently we can use decltype to automate.
using PersonIterator = decltype(persons.begin());
std::vector<PersonIterator> iterators;
Code Snippet 28: Demonstration of decltype vs typedef.
  • Trailing Return Type c11

    Sometimes with generic functions the return type is determined based on an expression of the parameter types. For situations like this we need to attach a trailing return type since the expression for it needs to know the names of the parameters.

    // This is needed because a and b aren't declared until after the return
    // type is already specified.
    template<typename A, typename B>
    auto add(A a, B b) -> decltype(a + b) {
        return a + b;
    }

    Note: The trailing return type syntax is obsolete since C++14. Now simply auto should be able to deduce the value without any extra declarations.

Lambdas

Lambdas are a built-in way to declare short anonymous functions. Each lambda is filled in by the compiler as its own unique type. No two lambdas, even with the exact same parameter list or body, will have the same type.

// CAPTURE PARAM-LIST [-> RETURN-TYPE] { BODY }
const auto add = [](int a, int b) -> int {
    return a + b;
};

// Return type can be inferred automatically and local variables
// can be captured into a lexical binding.
const int foo = 10;
const auto addFoo = [&foo](int a) {
    return a + foo;
};

Internally lambdas work by declaring an anonymous struct whose fields are initialised from the capture arguments and that's given a operator()(...) that has the same interface as the lambda type. Marking a lambda as mutable makes these fields mutable.

Captures Reference

Table 10: Listing of different capture expressions for a lambda.
CaptureDescription
[]{}No captures
[&]{}Capture everything by reference
[a]{}Capture the variable a by copy
[&a]{}Capture the variable a by reference
[&, a]{}Capture everything by reference and a by copy
[=, a]{}Capture everything by copy and a by reference

Argument Passing

The type of a lambda cannot be determined at runtime. The compiler fills in the lambda definition with an equivalent class that stores the captured variables and provides an operator() matching the lambda body. To allow passing a lambda to another function you can use a template type for the function that the compiler will fill in for you.

int foo = 5;
auto lambda = [&foo](int bar) -> int { return foo + bar; };
lambda(10); //=> 15

// The equivalent type the compiler will instantiate for this.
//
// Note: The type of a lambda is totally unique. Instantiating
// the same lambda more than once will lead to different types
// for each of them, even if they have the exact same signature.
struct lambda_anon_type {
    int &foo;
    lambda_anon_type(int &foo) : d_foo(foo) {}
    int operator(int bar) { return foo + bar; }
}

Generic Lambdas c14

Allows placing auto as a parameter type in a lambda and lets you create generic lambdas.

#include <vector>
#include <numeric>

std::vector<int> ints = {1,2,3,4,5};
const auto add = [](const auto lhs, const auto rhs) {
    return lhs + rhs;
};
std::accumulate(ints.begin(), ints.end(), 0, add); //=> 15

Note: A generic lambda parameter such as [](foo&&) is a forward-reference, not an R-value.

Generalized Lambda Captures c14

You can also generate arbitrary data-members in the capture list for a generic lambda.

int m, n;
auto myLambda = [&i = m, j = n]{
    // i is exposed as a reference to m.
    // j is exposed as a reference to n.
};

This can be useful to create mutable fields in the generated lambda struct and then allow the lambda to modify over them. For example:

auto counter = [i = 0]() mutable { return i++; }
counter(); //=> 0
counter(); //=> 1
counter(); //=> 2
counter(); //=> 3
counter(); //=> 4

View Types

Are light-weight wrappers (shallow copies) around data types such as std::string that can be passed as read only views into those data-types very easily. This is useful to avoid the overhead of cloning all the data on those types. View types don't own the data they point to, they simply expose access to it.

Note: There's always the option to parameterise references to the desired type instead of passing a view into it, but this can be sub-optimal and may fail to compile if a cast to the desired type isn't possible.

Note: One could always use generics to approach this but that loses the nice interface of a concrete type (such as a substring operation on a string).

Warn: One danger of view types is that the underlying data may be modified after the view is created.

std::string str("foo bar")
std::string_view sv(str);
// error: This may reallocate the char* causing the
// existing view into it for ~sv~ to be modified.
str += "baz bag";

To avoid this you should use views as parameters to functions and not persist their values beyond the lifetime of the function. By convention we don't pass views by reference because their shallow copies it's efficient to copy them repeatedly.

String View

std::string src = "watermelon";
std::string_view sv(src);
// This doesn't modify the actual string,
// it just shrinks our view into it.
sv.remove_suffix(5);
sv; //=> "water"
src; //=> "watermelon"

Iterators and Iterables

C++ iterators have an interface similar to pointers. You have an initial iterator pointing to the first element of a collection. You can increment the iterator to access the next element and you can compare a pointer against a final value which it equals when the iterator is finished.

const static int count = 10;
int nums[count] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (int *it = nums; it != nums + count; ++it) {
    std::cout << *it << '\n';
}
Code Snippet 29: Demonstration of how pointer pased iteration can work. code:ptr-iter

This idiom for enumerating a collection by instantiating and moving across a pointer like interface works across all standard collection types and even basic C arrays. The standard library defines std::begin to return an iterator for the beginning of a collection and std::end to return an iterator for the end. These are supported by all collection types and most collections also provide member functions doing the same thing.

int nums[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (auto it = std::begin(nums); it != std::end(nums); ++it) {
    std::cout << *it << '\n';
}
Code Snippet 30: An equivalent implementation of code:ptr-iter using a std::begin and std::end instead.

For Range Loop c11

Is a syntactical improvement which automates some of the boilerplate involved when iterating a conventional collection. All you have to do is specify a type for each element of the collection, a name to bind it to, and a collection to iterate.

int nums[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (const int &it : nums) {
    // Notice, no need to explicitly dereference the element.
    std::cout << it << '\n';
}

Conventions

Prefer Prefix to Postfix in Increment/Decrement Operators

When possible prefer prefix to postfix in increment and decrement operations. This is because postfix actually creates a copy of the object being incremented and then returns it, but if the goal is simply to perform an increment or decrement then this would be unnecessary. Just increment and return the original value.

Foo &Foo::operator++() { // called for ++i
    this->data += 1;
    return *this;
}

Foo Foo::operator++(int ignored_dummy_value) { // called for i++
    Foo tmp(*this); // variable "tmp" cannot be optimized away by the compiler
    ++(*this);
    return tmp;
}
Code Snippet 31: Demonstration of the basic implementation of prefix and postfix operators.

Equality Operator as a Friend Function

Can be implemented as a member function or a friend function.

class Foo {
public:
    bool operator==(const Foo &other);
    // OR
    friend bool operator==(const Foo &a, const Foo &b);
};

The latter form is preferred because the argument types, if implicitly converted, will both be converted. On the other hand for the member function the argument type may-be implicitly converted but the containing object (this) will not be.

Standard Library

CPP added a standard library in C++98. Before that all classes and functions were defined in the global namespace. To mark this turn the CPP standard removed the .h suffix from standard headers so now you'll find both <vector.h> and <vector> available as includes. In general the .h versions are kept for backward compatibility, but you should use the newer non-.h versions.

iostream

Is the standard streams library for C++.

It exposes functions for creating generic streams that can be read from and written into, including with automatic type conversions.

#include <iostream>

std::ifstream ifs("/file/path/foo.txt");
if (!ifs.good()) {
    throw std::runtime_error("Failed to open file");
}

// Reading each line from the opened file stream.
std::string line;
while (getline(ifs, line)) {
    std::cout << line << '\n';
}

// Reading numbers using >> operator and outputting them with minimum
// two character width and '0' padding to the left (%02d).
int number;
while (ifs >> number) {
    std::cout << std::setfill('0') << std::setw(2) << number << '\n';
}

Warn: If a read fails, it doesn't mean the previously read value of a variable has been reset. Always check after a read whether the stream is falsy in which case the read failed.

The stream API is very generic. You pass objects such as setfill and setw to configure formatting for later arguments such as number. The API even accepts functions. std::endl is a function and when passed to a stream, it is actually passed as a function pointer, and the operator<< handler will call the function.

Warn: Avoid usage of std::endl. It causes the output stream to be flushed, which can have noticeable performance costs. Prefer '\n' when possible.

strstream

Is a generic class for constructing strings as streams.

#include <sstream>

std::stringstream ss;

ss << 100 << ' ' << 200;
ss.str(); //=> "100 200"

int foo, bar;
ss >> foo >> bar;
foo; //=> 100
bar; //=> 200

vector

Is a variable length collection type for CPP which uses continuous fixed length arrays under the hood. Vector assumes the collection type is copy constructable and copy assignable.

#include <vector>

std::vector<int> vec;
vec.empty(); //=> true // Empty at initialisation

// Add elements to the end of the vector, allocating more
// memory for it if the capacity of the vector is exhausted.
vec.push_back(5);
vec.push_back(10);
vec.push_back(8);

vec.size();     //=> 3 // How many elements are in the vector
vec.capacity(); //=> ? // How many elements the vector can currently store,
		//        essentially how much space the underlying array has
		//        been allocated by vector.

vec[0];      //=> 5 // Lookup, doesn't check the bounds of the vector.
vec.at(0);   //=> 5 // Lookup, throws std::out_of_range if out of bounds attempt
vec.front(); //=> 5 // Fetch first element in the vector
vec.back();  //=> 8 // Fetch last element in the vector

// A variant of push back which temporary object construction.
//
// For a user defined type such as Tuple<u,v> with a constructor
// taking two arguments: emplace_back(u,v) is equivalent to
// push_back(Tuple(u,v)). This works using [[https://en.cppreference.com/w/cpp/language/new#Placement_new][placement new]].
vec.emplace_back(10);

// Dereference the value at the end of the vector. This doesn't return the popped
// value because... TODO why? http://www.gotw.ca/gotw/008.htm
vec.pop_back();
// Dereference all the elements from the vector.
vec.clear();

// Raise the capacity such that it can store upto 1000 elements.
vec.reserve(1000)

// Accumulate all the values in vec, notice use of ~size_type~.
int sum;
for (std::vector<int>::size_type i = 0; i < vec.size(); ++i) {
    sum += numbers[i];
} // sum => 33

vec.begin(); // Gives an iterator pointing to the first element of a container.
vec.end();   // Yields an iterator pointing to one past the last element in the container.
	     // You can use this to check whether a search on the container exhausted all
	     // possible entries and failed.
// Note: It's guaranteed that for an empty container vec.begin() == vec.end().

Erase-Remove Idiom

Is an idiom for iterable collections which allows deleting elements from the collection. With Erase-Remove we use remove to move values satisfying some predicate to the end of a range and then call erase to delete values from that range forward them.

Note: Remove doesn't necessarily move values to the end, instead it overwrites earlier values that are to be erased with later values, and leaves junk data (most likely the original to-be-removed values) in the to-removed position.

std::vector v;
v.push_back(10);
v.push_back(3);
v.push_back(3);
v.push_back(9);
// Remove all values equal to 3 from the collection
std::vector<int>::iterator after = std::remove(v.begin(), v.end(), 3);
v; //=> {10, 9, ?, ?}
v.erase(after, v.end()); // Actually erase from the collection
v; //=> {10, 9}

list

Provides a doubly linked list implementation that's mostly compatible with the vector API, except it also provides a push_front operation.

#include <list>

std::list<int> list;

list.push_front(5);
list.push_front(10);
list.push_front(8);

list; //=> 8, 10, 5

tuple

Provides an arbitrary length finite collection of distinct possible types.

#include <tuple>

std::tuple<int, double> tup = std::make_tuple(5, 5.5);

// std::tie creates a tuple of l-value references and you can assign a
// tuple of matching types from an R-value to update the L-values.
int i;
double j;
std::tie(i, j) = tup;

// std::tuple_cat concatenates to tuples together element wise.
std::tuple<int, double, const char*, int> tup2 =
    std::tuple_cat(tup, std::make_tuple("foo", 9));

// Tuples support element wise comparison and equality operators.
tup <= std::make_tuple(5, 5.6); //=> true

utility

pair

#include <utility>

std::pair<double, int> pair(5.4, 3);
// Note: pair does provide a default constructor but that only works
// when both of the pair types also provide a default constructor.

p.first;  //=> 5.4
p.second; //=> 3

// make_pair is a helper that constructs a pair from its argument
// types. This was mostly useful pre-C++17 where you didn't get
// automatic type-deduction for class templates.
std::make_pair(5.4, 3) == pair; //=> true

map

Is an ordered key-value collection type.

The standard requires most map operations to have \( \mathcal{O}(N \log N) \), so their typically implemented as a self-balancing binary tree (red-black trees commonly).

#include <map>

std::map<std::string, number> ages;

ages.insert(std::make_pair("Mohsin", 22));

ages.find("Mohsin")->second; //=> 22 // Return an iterator to the pair with first="Mohsin"

ages.find("Alicia") == ages.end(); //=> true // Failed to find entry with key

ages["Meelak"] = 5; // Assign to an existing pair or create a new one
ages["Nishom"] + 5; //=> 5 // Automatically creates and saves a default pair for missing
		    //        entries (but requires value type to be default constractable)

ages.erase(ages.find("Nishom")); // Remove a value from a map.

iterator

std::vector<int> nums;

// All iterable collection types expose iterator types within their namespaces.
std::vector<int>::iterator       it  = nums.begin();
std::vector<int>::const_iterator itc = nums.cbegin(); // Read only iterator

// You can iterate through a collection using an iterator, similair to a pointer.
for (std::vector<int>::iterator it = var.begin(); it != var.end(); ++it) {
    std::cout << it << std::endl;
}
Table 11: The STL Iterator Hirearchy.
Iterator CategoryCapabilities
Random Access[], +=, -=, +, -, <, <=, >, >=
Bidirectional--
ForwardMultipass
Input->, ==, !=
OutputCopy constructor, =, *, ++

algorithm

Provides a suite of useful and reliable algorithms.

std::vector<int> v(3);
v.push_back(10);
v.push_back(0);
v.push_back(3);
std::sort(v.begin(), v.end()); // Sort in iterator range from start to end of vector.
v; // {0, 3, 10}
std::vector<int> v, result;
v.push_back(-3);
v.push_back(-2);
v.push_back(2);
std::transform(v.begin(), v.end(), std::back_inserter(result),
	       // Cast needed because compiler needs to know which
	       // overload it should target.
	       static_cast<double(*) double>(std::abs));

Links to this note