About this course
Basic tier: class invariants, the Rule of Three and virtual dispatch. Advanced tier: templates, the standard library and persistence - eight modules culminating in the LedgerLens capstone.
This is a 6-week short course - enough depth to build real projects and a portfolio piece, without a long commitment. It runs online in the August 2026 cohort (starting 1 August 2026) and is taught the EchoLens way: you learn by doing real, gradeable work rather than just watching lectures.
What's included
- Live, instructor-led online sessions across 8 weeks (20 hours total).
- Hands-on coding quests you solve inside the EchoLens browser compiler - nothing to install.
- Gems, stages and a leaderboard that keep you moving instead of grade anxiety.
- A verified certificate with a scannable QR code, ready to share on LinkedIn, when you finish.
- Completely free - no fee, just create an account and start.
What you will learn
Course outline - level by level
8 leveles, each with hands-on quests you clear in the portal.
- Level 1. Basic 1: Classes, Invariants and Encapsulation - An invariant is a statement about an object that is true from the end of its constructor to the start of its destructor - a balance is never negative, a date is always valid. Encapsulation exists to protect invariants, not to hide data for its own sake; a class that exposes setters for every field has encapsulation in syntax only. Member initializer lists matter because members construct in declaration order before the constructor body runs - assigning in the body means constructing twice. Key rules: - State the invariant in a comment above the class. If you cannot state it, the class has no reason to exist. - Members initialise in declaration order, not the order written in the list. - Mark single argument constructors explicit unless an implicit conversion is genuinely wanted. - Prefer a constructor that rejects bad input over a setter that validates after the fact. Worked example - a wallet whose invariant cannot be violated from outside: class Wallet { long paisa_; // invariant: paisa_ >= 0 public: explicit Wallet(long paisa) : paisa_(paisa < 0 ? 0 : paisa) {} bool withdraw(long amount) { if (amount <= 0 || amount > paisa_) return false; paisa_ -= amount; return true; } long balance() const { return paisa_; } };
- Level 2. Basic 2: Object Lifetime, Destructors and the Rule of Three - When a class owns a resource, the compiler generated copy operations copy the handle rather than the resource, so two objects believe they own the same memory and the second destructor releases it twice. The Rule of Three: if you need any one of destructor, copy constructor or copy assignment, you almost certainly need all three. Tying resource release to object destruction is the single most important idea in C++ - it makes cleanup automatic on every exit path including exceptions. Key rules: - Rule of Three: define the destructor, copy constructor and copy assignment operator together or none of them. - Copy assignment must handle self assignment and release the old resource before taking the new one. - Destruction happens in reverse order of construction, automatically, on every exit path. - A shallow copy of an owning class is a double free waiting for a destructor to run. Worked example - an owning buffer with all three operations defined: class Buffer { int* data_; std::size_t n_; public: explicit Buffer(std::size_t n) : data_(new int[n]{}), n_(n) {} ~Buffer() { delete[] data_; } Buffer(const Buffer& o) : data_(new int[o.n_]), n_(o.n_) { std::copy(o.data_, o.data_ + n_, data_); } Buffer& operator=(Buffer o) { std::swap(data_, o.data_); std::swap(n_, o.n_); return *this; } };
- Level 3. Basic 3: Inheritance, Hierarchies and Slicing - Inheritance says a derived object is substitutable for a base object everywhere the base is expected. If that is not true for your hierarchy, composition is the correct tool. Slicing is the classic trap: assigning a derived object into a base variable copies only the base part and silently discards the rest, which is why polymorphic collections store pointers or references rather than values. Construction runs base to derived; destruction runs derived to base. Key rules: - Substitution test: if a derived object cannot stand in for the base everywhere, do not inherit. - Slicing copies the base part only - store pointers or references instead. - Construction runs base to derived; destruction runs derived to base. - Protected means visible to derived classes only - use it sparingly, it widens the interface you must maintain. Worked example - slicing shown side by side with the correct form: Asset a = Equity{ "PSO", 1200 }; // sliced: Equity part discarded std::vector<std::unique_ptr<Asset>> book; book.push_back(std::make_unique<Equity>("PSO", 1200)); // correct storage
- Level 4. Basic 4: Virtual Functions, Interfaces and Dynamic Dispatch - A virtual function is resolved by looking up a pointer in a table attached to the object at run time rather than at compile time. That indirection costs one pointer per object and one lookup per call - negligible in almost every application. The rule that matters most: any base class intended for polymorphic deletion must have a virtual destructor, or deleting through a base pointer will run the wrong destructor and leak the derived part. Key rules: - A pure virtual function makes the class abstract - that class becomes an interface, not an implementation. - Any polymorphic base class needs a virtual destructor. - Mark overrides with the override keyword - it turns a silent signature mismatch into a compile error. - Cost of dispatch: one pointer per object plus one indirect call. Do not avoid it on speculation. Worked example - an interface and a polymorphic collection: struct Reportable { virtual ~Reportable() = default; virtual double value() const = 0; virtual std::string label() const = 0; }; double total(const std::vector<std::unique_ptr<Reportable>>& items) { double sum = 0; for (const auto& i : items) sum += i->value(); // dispatched at run time return sum; }
- Level 5. Advanced 1: Templates and Generic Programming - A template is not a function, it is a recipe the compiler uses to write functions on demand. Nothing is generated until the template is instantiated with concrete types, which is why template definitions live in headers. Template error messages are long because they unwind the whole instantiation chain - read them from the bottom, where the original call site is. Key rules: - Templates are instantiated on use - the definition must be visible, so it stays in the header. - Read template errors from the last line upward. - Specialisation lets one type take a different implementation without changing the call site. - Constrain templates where possible so misuse fails at the interface rather than deep inside. Worked example - a generic ring buffer with a bounds contract: template <typename T, std::size_t N> class Ring { T slot_[N]; std::size_t head_ = 0, count_ = 0; public: bool push(const T& v) { if (count_ == N) return false; slot_[(head_ + count_++) % N] = v; return true; } };
- Level 6. Advanced 2: Standard Algorithms, Maps and Lambda Closures - Every hand written loop is a small opportunity for an off by one error. Standard algorithms remove that surface and name the intent: a call to sort or accumulate tells the reader what is happening without reading the body. The capture clause is where care is needed - capturing by reference into something that outlives the scope is the standard way to create a dangling reference. Key rules: - Ordered map lookup is logarithmic; unordered map lookup is constant on average. - Capture by value copies at the point of definition; capture by reference must not outlive the referenced object. - The accumulate algorithm folds a range into one value and replaces most manual sum loops. - Prefer a named algorithm over a raw loop wherever one exists. Worked example - an analytics pipeline built from algorithms and lambdas: auto total = std::accumulate(tx.begin(), tx.end(), 0.0, [](double acc, const Tx& t) { return acc + t.amount; }); std::map<std::string, double> by_category; for (const auto& t : tx) by_category[t.category] += t.amount;
- Level 7. Advanced 3: Smart Pointers, Ownership and Serialization - A raw pointer says nothing about ownership, and that ambiguity is the root of most memory defects in large C++ code bases. A unique pointer says exactly one owner; a shared pointer says reference counted shared ownership; a weak pointer breaks the reference cycles that would otherwise leak. Serializing a graph needs stable identifiers - write nodes once and refer to them by identifier afterwards. Key rules: - Unique ownership by default; reach for shared ownership only when lifetime genuinely cannot be determined. - Two shared pointers referring to each other never reach zero - break the cycle with a weak reference. - Make the owning object with a factory helper rather than a bare allocation, for exception safety. - Serialising a graph needs stable identifiers. Worked example - ownership expressed in the signatures: std::unique_ptr<Node> make_tree(); void inspect(const Node& n); void adopt(std::unique_ptr<Node> n); std::weak_ptr<Node> parent;
- Level 8. Advanced 4: Integration and the Course Capstone - Integration in C++ is mostly about drawing the ownership map before writing the code: which object owns the store, which borrow from it, what happens to open references when an entry is deleted. A design where those answers are in the signatures rather than the programmer's memory survives change. A project that cannot be built by someone else in one command is not finished. Key rules: - Draw the ownership map first - every arrow is either owning, borrowing or observing. - Deletion must invalidate every borrow - design the interface so a stale borrow cannot compile. - One build command - if setup runs past three steps, the build is part of the defect surface. - Public interface documented at the header; implementation detail never leaks into it. Worked example - interface that makes stale borrowing impossible: class Ledger { std::vector<Entry> entries_; public: std::size_t add(Entry e) { entries_.push_back(std::move(e)); return entries_.size() - 1; } const Entry* at(std::size_t i) const { return i < entries_.size() ? &entries_[i] : nullptr; } };
How you submit: Coding quests solved in the built-in EchoLens compiler.
Who it's for
Advanced C++ Programming suits learners at a beginner to intermediate level who want a practical, project-based route into Advanced C++ Programming. You need only a browser and an internet connection - all coding runs inside the EchoLens compiler, so there is nothing to set up.
Certificate
Finish every stage and EchoLens issues a verified certificate carrying a QR code anyone can scan to confirm it on our site. You can add it to your CV or share it to LinkedIn in one click.