https://www.learncpp.com/

Intro

Lang stack looks like

  1. Machine lang, x86 or arm that only specific CPUs can understand
  2. (Assembler converts ass lang into machine lang)
  3. Assembly lang, human readable machine lang (also machine specific)
  4. (Compiler and or interpreter)
  5. High level lang

Cpp stack looks like [info](https://en.cppreference.com/w/cpp/language/translation_phases)

  1. Source file
  2. (Preprocessing)
  3. (Compiler)
  4. object files: machine language
  5. (Linker)
  6. Link all these object files together into a exe or a lib

Basics

By default int width = 5 assignment copies the value to right side to the variable of left side, it’s doing a copy-assignment. Copy initialisation has an overhead, and is used for function parameter and returns.

Can also do other initialisation syntax.

int d { 7 };   // direct-list-initialization (initial value in braces)
int e {};      // value-initialization (empty braces), set to 0

// And the crappier direct initialisation
int f (4);
// List init disallows narrowing conversions! Beautiful!
int f (4.5); // somehow allowed?

Maybe a little surprising is that x = 5 and cout << 5 are both operators and can be chained like a +.

Basics: functions and files

When returning from main you need to pull in some preprocessor macros to return.

#include <cstdlib> // for EXIT_SUCCESS and EXIT_FAILURE

int main() {
    return EXIT_SUCCESS;
}

Compiler declarations.

  • If you’re defining two functions, because the compiler works sequentially line by line you need to declare functions you’re going to use before you use them. Sometimes this isn’t possible with cyclic definitions. You can avoid this by having a function prototype declared ahead.
#include <iostream>
using namespace std;

int add(int x, int y);

int main(){
    cout << add(5,6) << "\n";
    return 0;
}

int add(int x, int y){
    return x + y;
}

Cpp has namespaces, it was a shit show having all the function names being hogged up by stdlib functions, so these were moved to the std:: namespace. yay!

Before compilation there is a preprocessing phase, that converts source code into translation units that are passed to the compiler.

  • Processes those directives e.g #include <iostream> and macros #define.
  • You can do if statements on these macro defines to choose what code gets compiled.

Header files are just files which have a bunch of forward declarations that the preprocessor will include in the file for us.

  • You should have a header guard so that your header is only inlined once
  • Best practice is to include the corresponding h file in the corresponding cpp file. To catch compilation errors early.
#ifndef SQUARE_H
#define SQUARE_H

int getSquareSides()
{
    return 4;
}

#endif

Debugging

Consider printing to cerr << instead of cout as it’s unbuffered so will always be emitted to out.

Consider doing linting.

Fundamental data types

Incomplete types are types that are declared but not defined with a set amount of memory to allocate, e.g void.

Be careful with implicit conversions, especially between unsigned and signed integers!

Base ints actually have undefined bounds depending on architecture as they only have a defined min of 16 bits, this is crap, fixed in cpp11 with fixed width integers int32_t. But as it’s not guaranteed that the smallest bounded ints are the fastest there’s even types that will just choose the fastest one for you, int_fast32_t.

Can write stuff in scientific notation 1.2 x 10^4 = 1.2e4.

Cpp does implicit type conversion everywhere, unsurprisingly type conversion creates new values. Implicit conversion happens on copy assignment, function parameters and returns. Brace initialisation prevents implicit type conversion. yay!

Constants and strings

Cpp has two types of constants, Named and literal constants.

On named consts, just do below. These are called type qualifiers.

const int temp {5};

You can also have literal constants. These can be specialised by adding those suffixes for a specific literal type. e.g 5.0f.

List of compiler optimisations.

On compiler time evaluation, whether something is a constant expression matters, main sharp edge here is that even tho const double has the word const only const integral variables are constant expressions. To avoid this sharp edge just use constexpr!

constexpr double gravity {9.8};

But this doesn’t work if you just call a function, so you can also declare functions to use this so that it is allowed to run at compile and runtime. Define these in your header files if used across a project as they are implicitly inline.

constexpr int bing(){
	return 5;
}

Now these constexpr functions just signal that the function can be evaluated compile time. How can we declare that it must be evaluated only at compile time? With consteval!

// CPP20
consteval int bong(){
	return 6;
}

Compilers also inline function to remove the function call overhead when possible. This does have issues, as compiler works on a per file basis it can’t inline stuff in other translation units. To fix this issue you can use the inline keyword on functions to declare that they can be defined in multiple translation units and the linker will dedupe as necessary, just shove them into your header files.

Cpp has c style strings and you should try to avoid them as they have a set of sharp edges, especially on assignment between them. Just use std::string and std::string_view. String will dynamically allocate more memory if it needs more space.

  • Don’t forget to cast length as it’ll be unsigned.
  • Do not pass strings by value, thats expensive!

Key thing to remember is that fundamental type copies are fast and Compound types are slow. To fix this string being expensive, cpp17 has std::string_view in <string_view> .h. This is a readonly string, especially nice to avoid cost in function parameters. Also has downsides, it is just viewing a string so when that string is destroyed it will just be dangling viewing at something undefined.

Functions return temporary objects that auto destroy at the end of the full expression containing the function call! keep that in mind!

Operators

Pre and postfix operators, the prefix one returns the incremented thing, postfix just returns a temporary copy of the non incremented thing. Just use prefix operator!

#include <iostream>

int main()
{
    int x { 5 };
    int y { 5 };
    std::cout << x << ' ' << y << '\n';
    std::cout << ++x << ' ' << --y << '\n'; // prefix
    std::cout << x << ' ' << y << '\n';
    std::cout << x++ << ' ' << y-- << '\n'; // postfix
    std::cout << x << ' ' << y << '\n';

    return 0;
}

Bit manipulation

Smallest addressable unit of memory is a byte, so bools are pretty inefficient, lets pack these values into bitsets and unsigned ints.

  • Bitset doesn’t even save space for small collection of bools, will just allocate more bytes to be faster.
  • When using shorts, theres a foot gun where bitwise operators will promote your types to ints. This will make some weird stuff with width dependant operations, ~ and <<. So do static casts!

When touching bits you can use binary literal constants. 0b0101! Only on cpp17.

Scope, duration and linkage

To avoid name collisions, you can define your own namespaces.

  • Best practice being using capitalised namespaces.
  • Hit relevant namespace from global with Foo::doSomething()
  • Hit global from within a namespace with ::doSomething()
    namespace Foo // define a namespace named Foo
    {
      // This doSomething() belongs to namespace Foo
      int doSomething(int x, int y)
      {
          return x + y;
      }
    }
    

Local vars

  • Scope: visibility of identifier, local vars have block scope.
  • Duration: lifetime of variable, local vars are auto destroyed at the end of the block.
  • Linkage: no linkage, (can other declarations of the name refer to the same object)
  • You can update the duration to static with the static keyword.

Global vars

  • Try to define within namespaces to not pollute the global namespace
  • Try to start global vars with a g_ to indicate they are global variables.
  • Scope: visible with the file
  • Duration: will be destroyed when program ends
  • Linkage: internal or external linkage

Wtf is internal linkage?

  • Internal linkage = Can it be seen within a single translation unit.
  • Adding the word static or const, or constexpr keyword makes a variable internal linkage.
  • Functions also default to external linkage but you can avoid this with the same static keyword as well.

Wtf is external linkage?

  • Means that we can use it in other files if we do forward declaration.
  • You can do this using the extern keyword infront of global vars.

So whats the best way to share global variables?

  • Just create a inline constexpr int con {1} within a namespace in a header file.
  • You need the inline keyword to avoid the ODR violation

Namespaces don’t even need a name. You can have unnamed namespaces and these just mean that the namespace is treated as if it was the parent namespace. Useful for forcing things to have internal linkage.

Control flow

Usually if statements are evaluated at runtime, but what if you’re evaluating an expression that can resolve at compile time? Then you can do a compile time constepr if statement! This means that the if is removed away. Awesome!

#include <iostream>

int main()
{
	constexpr double gravity{ 9.8 };

	if constexpr (gravity == 9.8) // now using constexpr if
		std::cout << "Gravity is normal.\n";
	else
		std::cout << "We are not on Earth.\n";

	return 0;
}

Key sharp edge with switch statements is recognising the fall through behaviour, cpp switch statements when a case is triggered will just also execute the following case conditional blocks unless you have a break or a return.

When main function ends, exit() is called. You can also manually call this to terminate your function. Be warned it only cleans up static variables not local variables!

Theres one cpp random lib you should even consider using, mt19937 the Mersenne twister. Everything else is crap. Seed with std::random_device. [Some blogs even say that you should just not use the random lib](https://arvid.io/2018/06/30/on-cxx-random-number-generator-quality/)

Error detection and handling

Cpp has assert keyword to throw in tests. Also need to be careful not to have the compiler remove your tests!

#ifdef NDEBUG
    // If NDEBUG is defined, asserts are compiled out.
    // Since this function requires asserts to not be compiled out, we'll terminate the program if this function is called when NDEBUG is defined.
    std::cerr << "Tests run with NDEBUG defined (asserts compiled out)";
    std::abort();
#endif

assert(isLowerVowel('a'));
    
// can also add messages
assert(isLowerVowel('a') && "Lower vowel a was not correct")

Nicely summarises that theres four things you can do with an error.

  1. Handle it
  2. Pass the error back to caller
  3. Halt the program
  4. Throw an exception

Common failure is with handling user input with cin. Key thing here is on failed extractions, cin will keep failing until you call `cin.clear() and cin.ignore().

std::cin.clear(); // put us back in 'normal' operation mode (if needed)
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // remove any extra input

You can also move your runtime assertions to compile time, with static_assert.

Type conversion, type aliases and type deduction

Compiler will try to figure out if it can auto convert types into another.

  • One category is numeric promotions, smaller bits into larger bits, lossless conversions which are safe. So this is why you can shove a bunch of integral types into a int function and it’ll continue to chug along.
  • Another category is numeric conversions. Like ints to shorts to longs to doubles etc. This stuff can be lossy. Main thing to note is that depending on architecture ints can be 4 or 8 bytes meaning that int to double can even be lossy!
  • Key thing to note is brace initialisation bans the usage of some numeric conversions.
  • When you’re doing narrowing conversions, be explicit with static_cast<int>!

You can figure out the type of a var with typeid(x) from #include <typeinfo>.

Key call out is mixing unsigned integrals and signed integrals is a minefield. Mainly due to implicit conversions causing negative numbers into large signed numbers.

Theres five ways of doing named casts

  1. const casts: avoid
  2. reinterpret casts: avoid
  3. C style casts: avoid, will do whatever, will just go through all casts to see whats possible, looks like (double)x or double(x).
  4. static casts: USE THIS, does compile time checking!
  5. dynamic casts

You can create type aliases like

using Bing = long;

Function overloading and function templates

Compilers will mangle function names when doing it’s stuff, no standard on how args are encoded but its good to know.

Multiple overloaded functions can be called, with implicit conversion, this throws an error. Sometimes you don’t want certain overloaded functions to be callable, can do this by deleting it.

void print(char) = delete;

Or even delete all non matching overloads
template <typename T>
void print(T x) = delete;

You can do default args like so. This shit is done compile time, where compiler will just shove the number into the caller function. SHARP EDGE: You can’t declare it both in forward declaration and function definition. Best practice is to put it in the forward declaration, if one exists.

void foo(int x = 5)

foo()

You can push function args to compiler time constexpr with function non type template parameters. Like below.

template<int N>
int foo(){
	std::cout << N << "\n";
}

Include your templates in header files when you need them in multiple files.

Compound types: references and pointers

All expressions have a type and a value (2 + 5) e.g for that it has a type of int and a value. Values are related to assignment 5 = x is invalid. lvalue just means that it’s identifiable, so can be accessed by an id, ref or pointer. rvalues can’t be identified and have to be used immediately, like function returns.

Variable assignment works because you need to assign to a lvalue and any lvalue assigned on right side is auto converted to a rvalue.

References are just aliases for an object. You can get a lvalue reference with a &.

When possible pass a const reference over a non const reference. This lets you pass in modifiable lvalues and non modifiable lvalues.

Key takeaway is for function args use std::string_view instead of std::string. This is much cheaper for whatever string you get, no crappy conversions.

Pointers!

  • Get the address with &. Youre boxing it up.
  • Open up the address with * Youre tearing it open.

When talking about null pointers, don’t use NULL thats from C, use nullptr!

Huh didn’t realise but cpp has optionals how nice. std::optional. Yay the value or is kinda like my favourite nullish coalescing.

std::cout << *o1;             // dereference to get value stored in o1 (undefined behavior if o1 does not have a value)
std::cout << o2.value();      // call value() to get value stored in o2 (throws std::bad_optional_access exception if o2 does not have a value)
std::cout << o3.value_or(42); // call value_or() to get value stored in o3 (or value `42` if o3 doesn't have a value)

Compound types: enums and structs

Program defined types are enums and classes.

  • Capitalise their name.
  • Define these in header files. It’s not sufficient to do forward declarations because the compiler needs to see the entire definition to figure out how to do shit with the type.

Cpp supports two kinds of enums

  1. Unscoped enums
  2. Scoped enums (USE THESE)

Unscoped enums: enum Name{};

  • Call out is to avoid doing full caps enum naming as full caps is used for preprocessor macros.
  • Will be implicitly converted into integrals.
  • Has issue as all unscoped enums are polluting global namespace. So just throw them into a namespace{} so they don’t do this.
  • Make the default enum have a value of 0 so that default initialisation will select it.

Scoped enums: enum class Name{};

  • Fixes unscoped enum type safety!
  • Not a real CLASS!
  • Not polluting global namespace!

Cpp supports two ways of grouping data

  1. Structs
  2. Classes

One grenade with list aggregate initialisation is that its just positional so if you update your struct, anything calling it can blow up. There is keyword based initialisation.

Callout, you should have struct members be owner variables, not viewers. So you have good ownership on destruction and you don’t have dangling refs.

Unsurprisingly you can template your structs.

template <typename T>
struct Pair
{
    T first{};
    T second{};
};

Classes: intro

If your class has no data, just use a namespace.

If you want static classes, you need to add a const to construction and to member functions. Basically just tag your non mutating functions.

#include <iostream>

struct Date {
    int year {};
    int month {};
    int day {};

    void print() const // now a const member function 
    {
        std::cout << year << '/' << month << '/' << day;
    }
};

int main() {
    const Date today { 2020, 10, 14 }; // const
    today.print();  // ok: const object can call const member function
    return 0;
}

struct and classes are pretty much identical in cpp, one difference is visibility. Structs are default public, classes are default private.

General best practice of naming:

  1. m_var for private class variables.
  2. s_var for local static variables
  3. g_var for globals

For cpp visibility looks like:

Access Member access Derived class access Public access
Public y y y
Protected y y n
Private y n n

DO NOT return a non const ref in a class getter! DO NOT save a returned reference from a function as a reference! (That shit is a temporary object)

You initialise members in your constructor with a member initialiser list. Like below.

class Fraction
{
private:
    int m_numerator {};
    int m_denominator {};

public:
    Fraction(int numerator, int denominator):
        m_numerator { numerator }, m_denominator { denominator }
    {}
};

Theres a sharp edge in handling failures in constructors because you usually can’t

  1. Resolve the error in the function
  2. Pass the error back, as we have no return
  3. So we usually have to either throw an exception or halt the program.

Similar syntax to member initialisation you can call other constructors as well. So you can chain constructors.

On casting between types.

  1. Use static cast to create fundamental types
  2. List initialisation with the braces when creating classes

Theres something called the rule of five. If you need one of these you likely need to implement all of these.

  1. Copy constructor
  2. Destructor
  3. Copy assignment operator
  4. Move constructor
  5. Move assignment operator

Class constructors will auto be used to implicitly convert types, to prevent this use the explicit keyword.

Classes: more stuff

Prefer to define your class definitions in a header file with the same name as the class and define non trivial member function in the source file with the same name as the class.

  • This split improved compilation time! Because any code changes in header files means anything that takes in that header needs to be recompiled.

Clean up your shit with destructors.

public:
    Simple(int id)
        : m_id { id }
    {
        std::cout << "Constructing Simple " << m_id << '\n';
    }

    ~Simple() // here's our destructor
    {
        std::cout << "Destructing Simple " << m_id << '\n';
    }

    int getID() const { return m_id; }
};

Class members can have static variables, but these are pretty much just global vars that live inside the class scope.

friend keyword exists to allow access to private data in classes, is interesting but looks like something to create spaghetti code

Dynamic arrays: vector

Vectors cannot be pushed compile time using constexpr!

You can get a signed size of a vector with std::ssize()! You can also access vectors with vec.data()[int] to access elements with signed integers!

You actually don’t need to pass vectors as const references due to move semantics! Move semantic happens when

  1. The type supports move semantics
  2. Object initialised with an rvalue object of the same type
  3. The move isn’t elided

When creating an object to insert into a vector, use emplace_back().

Just don’t use a bool vector, use a bitset or char vector instead.

Fixed arrays: array, c-style

When you need a fast array use a constexpr array<int, 5>.

To pass arrays around you can use templates which the compiler uses to pass in the array length.

#include <array>
#include <iostream>

template <std::size_t N> // note: only the length has been templated here
void passByRef(const std::array<int, N>& arr) // we've defined the element type as int
{
    static_assert(N != 0); // fail if this is a zero-length std::array

    std::cout << arr[0] << '\n';
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 }; // use CTAD to infer std::array<int, 5>
    passByRef(arr);  // ok: compiler will instantiate passByRef(const std::array<int, 5>& arr)
    return 0;
}

References aren’t objects so you can’t have a list of them, so use a std::reference_wrapper to create an object of a reference, or short hand std::ref().

C style arrays DECAY into pointers on some assignment! This is an issue as sizeof stops working on decayed pointers.

When you need to traverse a 2d array std::mdspan is useful for handling multi dimensional data.

Iterators and algorithms

Theres three main categories of algos in <algorithm>.

  1. Inspectors: non modifying viewing
  2. Mutators: mutate it
  3. Facilitators: eh

std::find: find something std::find_if find on condition std::count, std::count_if same but for counting std::for_each do something on every element

You can time stuff with below

#include <chrono> // for std::chrono functions

class Timer
{
private:
	// Type aliases to make accessing nested type easier
	using Clock = std::chrono::steady_clock;
	using Second = std::chrono::duration<double, std::ratio<1> >;

	std::chrono::time_point<Clock> m_beg { Clock::now() };

public:
	void reset()
	{
		m_beg = Clock::now();
	}

	double elapsed() const
	{
		return std::chrono::duration_cast<Second>(Clock::now() - m_beg).count();
	}
};

Dynamic allocation

Theres three ways of allocating memory

  1. Static: static and global vars
  2. Automatic: stack 1MB, function variables and parameters
  3. Dynamic: heap?

Stack is fast as compiler knows the address of stack objects, so can access directly. Heap access is slow because we need to lookup where the object is, then to get the value.

Just new and delete for heap allocation and don’t leave dangling pointers.

  • And new[] delete[] for arrays

Functions

Functions are also pointers, use std::function from <functional> to pass around functions.

The memory a program uses is divided into a few segments:

  1. The code: where compiled program sits in memory, read only
  2. The bss/uninitialised data: where the zero initialised and static vars are stored
  3. The data/initialised data: where the initialised global and static vars are stored
  4. The heap: dynamic memory
  5. The stack: function parameters, local vars

You eat CLI args with

int main(int argc, char* argv[])

A problem with cpp is we sometimes want a list of parameters unknown at compile time like python! But ellipsis has no type checking and sucks.

  1. cpp11 parameter packs and variadic templates are possible solutions
  2. You should go with cpp17 fold expressions + parameter packs!

Cpp has lambdas. If you’re asking what that [] block is thats the capture clause, thats how you pass in variables from outside scope into your lambda.

auto isEven{
  [](int i)
  {
    return (i % 2) == 0;
  }
};

return std::all_of(array.begin(), array.end(), isEven);

Cpp <functional> has a bunch of useful lambda functions you can just plug in easily.

Operator overloading

Theres three ways to overload operators

  1. With member functions <- recommended for mutating operations
  2. With friend functions
  3. With normal functions <- recommended for non mutating operations

Key thing to consider when overloading cin >> is to extract everything you need then do assignment to avoid partial extractions.

Move semantics and smart pointers

The compiler will provide a copy constructor and copy assignment operator by default, which does shallow copies. This is a grenade for classes which touch dynamic memory.

The key insight is that if we’re calling a function with a l-value argument, we need to pass in a l-value, this means that we need to copy and create an object. But if the arg is an r-value then we know the thing that is provided is temporary and we can have the temporary resources be destroyed, so we can move those resources.

So using this how can we get a pointer class which is unique and auto cleaning? Just implement the move operators and delete the copy operators. ezpz.

So move semantics is cool, how can I just use it in my day to day? How can I do move assignment with this ref I have? Just use std::move(a) in <utility>. This gets you a rvalue that you can use.

Theres three smart pointers in <memory>

  1. unique_ptr
  2. shared_ptr
  3. weak_ptr

When using unique_ptr

  • ALWAYS allocate smart pointers on the stack so they are auto destroyed!
  • *ptr: to get your resource from a unique_ptr.
  • To get a unique pointer just use std::make_unique<stuff>() this creates your object for you simply!

When using shared_ptr

  • Always make a copy of existing shared_ptr, thats how it shares the referencing counter.
  • Use make_shared<stuff>()

When using weak_ptr

  • Lets you avoid shared pointer cycles
  • Need to lock() to get a value, and need to check expired() as you aren’t the owner.

Inheritance

Cpp has class inheritance. And you chain constructors with the member initialisation syntax.

class Derived: public Base
{
public:
    double m_cost {};

    Derived(double cost=0.0, int id=0)
        : Base{ id } // Call Base(int) constructor with value id!
        , m_cost{ cost }
    {
	    Base::do_thing(); // this is how to call base class members
    }

    double getCost() const { return m_cost; }
};

Virtual functions

Virtual functions are a special type of member function that when called will resolve to the most derived version of the function for the actual type of the object. THIS only works when called on a pointer of reference.

  • key sharp edge is due to the cascading nature of constructors and destructors you cant call virtual functions from them, as you’ll call something that doesn’t exist.

Cpp also has two other inheritance related features

  1. override: to tag functions so that you get a compile error when you intended to override and it didn’t work
  2. final: prevent people from inheriting a member

Key callout is that you should make your inheritance class destructors virtual! So they do clean up crap in order!

Theres two flavours of binding

  1. early binding: the compiler can figure out where we need to jump for a function call
  2. late binding: the compiler can’t figure it out, so we need to do it at runtime

This late binding is usually implemented with a vtable, or dispatch table.

You can do abstract virtual functions in cpp as well. tada.

virtual int getValue() const = 0;

When trying to get back a derived class that you’ve casted to a base class, use a dynamic_cast to safely check if its possible and get the derived class back.

Templates and classes

With non template classes

  1. Put the class def in header
  2. Put the member functions in code file

With template classes you need everything in the same place

  1. Throw everything into the header file

Exceptions

Cpp has try catch blocks very similar to java.

#include <iostream>
#include <string>
#include <exception> // for std::exception

int main()
{
    try
    {
        throw -1;
    }
    catch (const std::exception& exception)
    {

        std::cerr << "We caught an exception with value: " << x.what() << '\n';
    }
    return 0;
}

Usual recommendation is to have a catch all block in main, which is disabled on debug builds. And to throw back the same exception upwards you just have an isolated throw; statement.

Just throw these when you need to

Input and Output

Cpp has input and output streams.

  1. istreams, handle input
  2. ostreams, handle output
  3. iostream, handle both

Streams can be used with manipulators, like below that limits the num of chars are read

#include <iomanip>
char buf[10]{};
std::cin >> std::setw(10) >> buf;

With istreams you can get() and getline() to get stuff from it.

  • Check number of chars read with gcount()

With ostreams you can flag stuff with std::cout.setf(std::ios::showpos).

For string specific streams theres 6

  1. istringstream
  2. ostringstream
  3. stringstream
  4. And their wide char variants

Misc and Libraries

Usually a cpp library comes in

  1. A header file
  2. A precompiled binary that contains the implementation

Theres two kinds of libraries

  1. Static: (.lib or .archive) Libraries are compiled into directed into your program
  2. Dynamic: (.so or .dll) Libraries not compiled into program. Not linked and so you need to explicitly load and use them, or use an import lib: a static lib that automatically loads the library for you.

To use cpp libs you have four steps

  1. Download the precompiled package for your os.
  2. Install the lib, into your computer
  3. Tell your compiler where to find header files like /usr/include
  4. Tell linker where to find libraries /usr/lib

Now for your IDE

  1. If using static libs, tell linker what lib to link
  2. #include the header files
  3. If using dynamic libs, LD_LIBRARY_PATH env var needs to have your library path.

Cpp versions

cpp11 BIG

  • auto
  • enums
  • lambda expressions
  • move semantics
  • smart pointers

cpp14

  • aggregate member initialisation

cpp17

  • class template arg deduction CTAD
  • More std types, like string_view

cpp20 BIG

  • chrono improvements
  • Even more compile time support for non if statements
  • Modules
  • Enum classes!

cpp23

  • mutidimensional subscript
  • range algorithms