A Tour of C++

1. The Basics

  • Initialization

    double d1 = 2.3;
    double d2 {2.3};
    double d3 = {2.3}; complex<double> z = 1; complex<double> z2 {d1,d2}; complex<double> z3 = {d1,d2};
    
  • Use auto to have the compiler deduce the variable’s type:

  • Immutable values

    • const: value can be calculated at run time
    • constexpr: value must be calculated at compile time
    • 400
    • Expressions within a constexpr must themselves be constexprs
    • Functions can be constexpr too, but with restrictions
      • To be constexpr, a function must be rather simple and cannot have side effects and can only use information passed to it as arguments. In particular, it cannot modify non-local variables, but it can have loops and use its own local variables.
      • 400
  • Loops

    • Range over a collection:

      for (auto x : v)
          cout << x << '\n';
      
    • This copies each element of v into x though. To do this by reference instead:

      for (auto &x : v)
          cout << x << '\n';
      
  • Functions copy arguments by default, use explicit references to avoid this

    • Use const references to avoid copying when you don’t want to mutate args
  • nullptr, not NULL

  • if statements can introduce variables

        void do_something(vector<int>& v) {
        if (auto n = v.size(); n!=0) {
            // ...
        }
    }    
    
  • 700

  • References vs. pointers

    • References appear to be like pointers but somewhat more ergonomic in that dereferencing is automatic
    • References can’t be reassigned after initialization
    • 500
    • 500
  • Initialization vs. assignment *

    In general, for an assignment to work correctly, the assigned-to object must have a value. On the other hand, the task of initialization is to make an uninitialized piece of memory into a valid object.

2. User-Defined Types

Structs

  • Initialization

Classes

  • Basic structure: 400
  • There is no fundamental difference between a struct and a class; a struct is simply a class with members public by default. For example, you can define constructors and other member functions for a struct.

Unions

  • The compiler doesn’t keep track of which union field is currently active, so unions are typically combined with a type enum, called a tagged union
  • Use variants to do this directly:
    variant<int, string> v = "abc";
    if (holds_alternative<int>(v)) {
    	// v is an int
    } else {
    	// v is a string
    }
    

Enums

enum class Color { red, blue, green };
Color col = Color::red;

3. Modularity

  • Header files containing declarations but not implementations

    • 500
    • The use of #includes is a very old, error-prone, and rather expensive way of composing programs out of parts. If you #include header.h in 101 translation units, the text of header.h will be processed by the compiler 101 times. If you #include header1.h before header2.h the declarations and macros in header1.h might affect the meaning of the code in header2.h. If instead you #include header2.h before header1.h, it is header2.h that might affect the code in header1.h

  • Modules are a replacement that will be part of C++20

    • Only compiled once, even with multiple imports
    • Doesn’t need the header/impl split, control visibility from a single file
  • Namespaces

    • Use :: as an explicit namespace qualifier
    • using to bring a namespace into (lexical) scope
    • namespace to define a namespace/members
  • try/catch for error handling

    • Destructors are invoked as the call stack is unwound
    • Exceptions appear to all inherit from class Exception
    • Use the noexcept modifier for functions that should never throw (the process is terminated if the function throws)
  • The assert macro only performs assertions in “debug mode”

  • static_assert for compile-term assertions (argument must evaluate to a constexpr)

  • Does the act of passing a reference by value copy the reference itself or the target?

  • Return-by-reference either using pointers (bad form, apparently) or move constructors

  • Destructuring/binding struct args/returns:

    struct Entry {
    	string name;
    	int value;
    };
    
    Entry foo() {
    	string s = "foobar";
    	int i = 0;
    	return {s, i};
    }
    
    void bar(Entry e) {
    	auto [n, v] = e;
    }
    

4. Classes

  • C++ has the equivalent of pointer/value receivers using the const qualifier against a method:

    class Example {  
    	double x, y;
    
    	// This method can't modify the implicit `this`
    	double foo() const { return x; } 
    
    	// This method _can_ modify the implicit `this`
    	double bar() { return y; } 
    }
    
  • new

    • Use Example e {args}; to create an object that goes out of scope when the current lexical block is closed
    • Use Example e = new Example(args) to create a pointer to an object that needs to be manually deleted
  • Inlining

    • Functions defined in a class are inlined by default.
    • It is possible to explicitly request inlining by preceding a function declaration with the keyword inline.
  • Destructors are defined as ~Class()

  • Use delete to collect a single object, delete[] to collect an array

  • RAII: use new behind abstractions so allocations occur in a constructor and deletions happen automatically in a destructor

  • Accept std::initializer_list in a constructor to be able to do this:

    class Vector {
    public:
        Vector(std::initializer_list<double>);
    };
    
    Vector v1 = {1, 2, 3, 4, 5};
    Vector v2 = {1.23, 3.45, 6.7, 8};
    

Abstract Classes

  • Virtual functions are allowed to be redefined in subclasses

    • An = 0 at the end denotes a “pure virtual” function, which must be redefined
    • A class with a pure virtual function can’t be instantiated and is called an abstract class
  • Use : to subclass:

    class Container {
      public:
    
      virtual void inspect() {
    	std::cout << "HELLO!\n";
      }
    
      virtual int foo() = 0;
    };
    
    class LinkedList : public Container {
      int foo() {
    	return 0;
      }
    };
    
  • Use the override keyword to make redefinition explicit

  • Functions can be passed pointers/references to abstract types but not directly by value

  • vtables

    • Given a reference to an abstract type, how does the compiler know which concrete implementation is backing it, say for a method call?
    • Every concrete subtype of an abstract type has a virtual function table (or vtable) defined, which points to all concrete implementations that that type defines for virtual definitions in a supertype
    • Every object links to the right vtable for its concrete type, so an abstract reference can find the right method to call
    • 500
  • Abstract classes typically define a virtual destructor, so if a pointer to the abstract class is deleted, the correct concrete destructor is called.

  • dynamic_cast<Type>() for runtime type checking *

    If the cast is successful, dynamic_cast returns a value of type new-type. If the cast fails and new-type is a pointer type, it returns a null pointer of that type. If the cast fails and new-type is a reference type, it throws an exception that matches a handler of type std::bad_cast

  • Pointers aren’t destroyed when they go out of scope, so they’re typically wrapped in a value class (RAII, instantiate without new), with the pointers deleted in the destructor. unique_ptr allows doing this without defining a custom class every time.

5. Essential Operations

  • 400 *

    Except for the “ordinary constructor”, these special member functions will be generated by the compiler as needed.

    • =delete to disable this behavior
  • A good rule of thumb (sometimes called the rule of zero) is to either define all of the essential operations or none (using the default for all).

Implicit Conversions

  • C++ looks at constructors to figure out if a conversion is possible.

    // 1. Given a class like this
    class Vector {
    	Vector(int size) {
          // Initialize vector
    	}
    };
    
    // 2. This code will call that constructor
    Vector v1 = 7;
    
  • Use the explicit keyword against the constructor to avoid this sort of implicit conversion

Copy

  • The default copy constructor individually copies each member, which can be invalid in some cases (like for classes that maintain a pointer to data on the heap).

  • Copy constructors vs. copy assignment

    class Shape {
    public:
      Shape(int n) : n {n} {}
    
      // Copy constructor
      Shape(Shape& s) {
        cout << "COPY CONSTRUCTOR" << "\n";
        this->n = s.n;
      }
    
      // Copy assignment
      Shape& operator=(Shape& s) {
        cout << "COPY ASSIGNMENT " << "\n";
        this->n = s.n;
        return *this;
      }
    
      int n = 50;
    };
    
    int main() {
      Shape s1{90}, s2{s1}, s3{500};
      s3 = s2;
    
      cout << s1.n << " " << s2.n << " " << s3.n << " \n";
    }
    
    • Shape s2{s1} invokes the copy constructor
    • s3 = s2 invokes copy assignment

Move

  • A function that wants to return local data can (typically) only return a value (which is copied) or pointer.
  • Use a move constructor / move assignment to avoid this, which then (transparently) changes return values to be moves instead of copies.
    • Move constructor: Vector(Vector&& a)
    • Move assignment: Vector& operator=(Vector&& a)
  • The && is not a reference to a reference. A reference isn’t a standalone entity like a pointer but simply an aliasing mechanism, so this doesn’t make any sense. Instead:
    • & is an lvalue reference: a reference that can appear on the left side of an =; a reference that can be assigned to
    • && is an rvalue reference: the opposite of an lvalue reference; a reference that cannot be assigned to, like the return value of a function
  • The && implies that this reference isn’t assignable anywhere else, and so the move constructor can safely modify/delete/etc. it
  • Modern compilers use return-value-optimization to optimize this away (a move without a move constructor) where possible; use -fno-elide-constructors to disable this
  • std::move to force a move where the compiler has imperfect information
    • 300
    • (Also so much of Rust is making a lot more sense now!)

RAII

  • Before resorting to garbage collection, systematically use resource handles: let each resource have an owner in some scope and by default be released at the end of its owners scope.
  • In C++, this is known as RAII (Resource Acquisition Is Initialization) and is integrated with error handling in the form of exceptions.
  • Resources can be moved from scope to scope using move semantics or smart pointers, and shared ownership can be represented by shared pointers
  • Yes, agree. I think RAII is one of the greatest inventions in programming ever a… | Hacker News

Iterators

  • Use begin and end to iterate a container
  • Iterators overload ++ to next
for (auto p = c.begin(); p!=c.end(); ++p)
	p = 0;

User-Defined Literals

  • Use a suffix to override the implicit type of a constant
    • "Surprise"s is a std::string insteasd of a const char[10]
    • 123s is of type second instead of int
  • 400
  • 800

6. Templates

  • Generics, basic syntax: 500

  • Method definitions outside the main class scope need the template prefix: 300

  • Monomorphized at compile time, no runtime cost

  • Each concrete version of a templated entity is a specialization

  • Use concepts to constrain allowable types: 400

  • Templates can accept value arguments, not just types, but values must evaluate to constants:

  • C++17 can infer template parameters:

    // Old
    pair<int, double> p = {1, 1.25};
    
    // New
    pair p = {1, 1.25};
    
  • Functions can be templated too

  • Classes can implement operator() so their objects can be called like a function; these are function objects

  • Lambdas are sugar over function objects

  • Lambdas that accept an auto parameter are effectively a generic lambda

  • Use lambdas to convert any imperative code to an expression

  • Compile-time ifs are like AST-aware ifdefs: 300

7. Concepts and Generic Programming

Stopping here, it’s proving difficult to figure out which bits of code samples are part of the stdlib and which aren’t.

Edit