C++ is a multi-paradigm language that lets you solve a problem in different ways, and this course will teach you the best practices.
C++ is a multi-paradigm language that lets you solve a problem in different ways, and this course will teach you the best practices.
Furthermore, C++ has been re-invented again. C++2a is the largest extension to the language since C++11, and it almost feels like a new language. Developers who master the new features will be able to write cleaner, faster and concurrent code. In this course, you will learn about the best practices of C++ programming, including project structure, designing interfaces and classes, C++ Core Guidelines, and the most recent language standard. The new features are numerous and cover almost every area of the language: modules let us organize our programs better; concepts help us create cleaner APIs; ranges forever change how we look at containers; concurrency features, such as coroutines, bring parallel and concurrent programming to a whole new level.
You will get plenty of practical experience with short, real-world code examples. By the end of this course, you will be ready to create better software using C++.
About the Author
Georgy Pashkov is a passionate software developer and team leader. The author has been successfully programming with C++ for over 10 years.
Throughout his career, Georgy has worked on many projects developed using C++, mainly in the supply chain industry. His experience covers desktop, server, and mobile applications for multiple platforms. He is passionate about creating high quality, reliable software.
Georgy was raised in Karelia, Russia. He earned his specialist degree in CS at Petrozavodsk State University and moved to Belgium shortly after graduation. With many years of experience in software development, he decided to share some of his skills.
This video will give you an overview about the course.
C++ is a multi-paradigm programming language. We learn about different paradigms (styles), and how C++ supports them.
• Programming styles supported by C++
• Criticism of C++
• Your C++ knowledge as a toolbox
Every new C++ standard adds support for different programming styles. Understanding the evolution of C++ can improve programming skills.
• How C++ started?
• Overview of features added in each new standard
• Features of the new C++2a standard
You only need a compiler and a text editor to begin working with C++. Professional development requires more tools. This video provides an overview of the most important ones.
• Hardware and build acceleration
• Version control, debuggers, and IDEs
• Other useful tools
In this video, we learn what is physical project structure and why it is important.
• What is project structure?
• Why is good structure important?
• What are the elements of project structure?
In this video, we learn how to organize source code of a small project.
• Overview of the daytime project
• Organize code into packages
• Separate package interface from implementation
In this video, we learn about the factors that affect compilation speed and get some practical guidelines for improving it.
• Factors affecting compilation speed and common tools to get quick wins
• Precompiled headers
• How to write code that compiles faster?
In this video, we learn about one of the most common C++ tricks to improve compilation speed and hide implementation details.
• Add Pimpl to one of the classes
• Fix const correctness
• Pros and cons of the Pimpl idiom
In this video, we learn about the biggest change to C++ in decades.
• Writing our first module
• Consuming the module
• Visibility and reachability
In this video, we learn how modules can be split into implementation and partition units.
• Implementation units
• Partitions
• Modules summary
Application programming interfaces are to programmer what UI is to the end user. Creating good interfaces improves modularity and promotes code reuse.
• What is an API and why we need it?
• What makes a good API?
• How to design a good package?
Abstraction hides complexity and preserves only the information that is relevant in the context. We create leaky abstractions when we fail to hide information that is irrelevant. This video is an exercise in creating an abstraction.
• Abstractions and leaks in them
• Case study: designing a logger class
• Abstraction leaks in the logger
Most languages offer a couple of ways of passing data, e. g. by value and by reference. C++ offers many more. How to choose the right way in every case?
• Ground rules for passing arguments
• When to pass/return by reference, by value, by rvalue reference, and by pointer?
• How to avoid excessive copy and when to pass raw pointers?
Interface is a contract between two parts of a program. Most functions limit what inputs are valid, while function callers expect a limited range of return values. Most classes have a limited number of states. A good interface must enforce these rules clearly and unambiguously. Up to half of the code is error checking, so it makes sense to master the craft.
• Checking preconditions and postconditions
• Three ways of error handling
• Exceptions: Rules and gotchas
C++ lets us design an interface using different styles (or paradigms). You can use templates, virtual functions, Pimpl, good old C-style functions, or a combination. Which way is better? Depends on the situation. This video provides an overview, with examples from popular C++ libs.
• Template-based interface
• Object-oriented interface
• C-Style interface
Sometimes we want to share a package, without sharing its source code. And sometimes we want to push updates to our shared libraries, without requiring that customers re-build their applications. This requires a stable application binary interface.
• Application Binary Interface (ABI)
• Things that can break an ABI
• Using C-Style interfaces to provide a stable ABI
What is the difference between struct and class, and which one to use? What’s an invariant and why is it important? Where to put error checking code?
• Struct versus class: Which one to use?
• Class invariants by example
• Public and private
Constructors, destructors, and assignment operators are all about object’s lifetime. Sometimes they are automatically generated, sometimes not. This video, provides instructions on how to deal with them.
• The rule of zero: Stick to the defaults if you can
• Rules for implementing constructors and factory methods
• Rules for implementing destructors
Copy creates a duplicate of an object. Move transfers ownership of resources. We can control the behaviors of copy and move by writing copy and move constructors (and operators).
• Rules for implementing copy constructors and assignment operators
• Rules for implementing move constructors and assignment operators
• Implementing a polymorphic copy constructor for class hierarchies
Inheritance is one of the cornerstone features of object-oriented design. We use it to represent a set of hierarchically organized concepts. How to design a good hierarchy?
• Drawing a hierarchy
• When to use inheritance and when to avoid it
• Interface and implementation inheritance
In this video we implement a dual hierarchy for server applications supporting multiple protocols
• Overview of what we will implement
• Implementing a hierarchy of servers
• What we learned by implementing the hierarchy?
Classes in a hierarchy are almost always allocated on the heap. Managing memory manually is error prone. STL provides a set of smart pointers that make the task easier.
• About RAII design pattern
• Why prefer smart pointers to raw pointers?
• Using unique_ptr for object ownership
Shared_ptr is a reference-counting smart pointer. It’s less efficient than unique_ptr, but lets us forget about complexities of memory management, and helps avoid double delete bugs.
• How shared_ptr works
• Using shared_ptr and weak_ptr, live coding demo
• Summary
A function should perform a single logical operation. Small functions are easy to maintain and promote code reuse. The most common anti-pattern is a function that does too many things.
• Why small functions are good?
• How small should a function be?
• How to give descriptive names to functions?
How many arguments should a function have, and how to pass them properly? There are simple ways for making the functions even better.
• How many arguments?
• Passing by value, by reference and by pointer
• Return values and other tips
Why use function objects together with <algorithm> instead of loops? How to write great lambda expressions easily, and how to master the capture block.
• Implementing search with a simple loop and with an algorithm
• Mastering the capture block
• Generic lambda expressions
In this coding demo, we use lambda expressions to implement callbacks, for adding logger capability to the server, without increasing coupling.
• About callbacks
• Std::function
• Using callbacks to implement logging in the server application
Some expressions can be evaluated during compilation. This lets us improve performance, and also compute constants, which improves code readability.
• About constant expressions
• What code can become a constexpr?
• Live coding: instant math using constexpr
Templates are one of the distinctive features of C++, and arguably the most complex one. When used correctly, templates make your code less complicated, and this is not a contradiction.
• Use templates to raise the level of abstraction: Express algorithms, containers, and ranges
• Live coding demo: Converting to templates
• Use templates to make code re-usable
When we use a template, we instantiate it. Let’s create a type that performs fixed-point arithmetic, parameterized with templates. Then let’s support that type in our math library
• Developing a fixed-point math class
• Adding arithmetic operators
• Using static asserts as “unit” tests
Our “equal” function uses an epsilon to compare values. This works well with floating point types, but it’s inefficient for fixed point. We can try to specialize the function template, but how to do this in a generic way?
• The problem with using epsilon for fixed-point types
• Using std::enable_if to distinguish between types
• Using type_traits for custom types
Templates make poor interfaces: typename is a wildcard. Concepts let us set requirements for template parameters. Template writer can easily describe what’s expected of the user. Concepts make template interface good.
• About concepts: new keywords and supported compilers
• Example: a concept for types with rounding errors
• Syntax of constraints and concepts
So far, we have explored a single use case for concepts. There are more benefits and more tools at our disposal. Some best practices are already defined too.
• Different ways to specify type requirements
• Most important guidelines when using concepts
• When to use concepts?
STL provides many containers, many of them similar. When to use std::vector, and when std::list? This video provides some answers.
• Contiguous storage: array and vector
• Details of std::vector, and why prefer vectors over lists
• String, string_view, and span
Search is the most common operation we perform on data. There are three common types of search: Linear, sorted (binary), and by hash.
• Linear search is the simplest way to find things, and often the fastest
• Overview of associative containers: Sets and maps
• Choosing a search algorithm for every situation. About exotic containers
Real-world programs manipulate data in numerous ways. You will need filtering, conversions, transformations, and so on. There is always a temptation to write a loop, but STL algorithms are preferable.
• How many ways are there to write a loop in C++?
• Live coding demo with multiple ways to implement the same algorithm
• Instead of writing a loop, see if you can solve the problem with <algorithm>
Most algorithms use a pair of iterators. Most containers can be thought of as a pair of begin() and end() iterators. Why not use a pair of iterators everywhere? Roughly, this is what ranges are all about.
• Installing range-v3 from GitHub
• Key concepts: ranges and pipelines
• Implementing our first pipeline with views and actions
You probably have more questions than answers at this point: how does the pipeline work? How many loops did we just write? What is the return value of a pipeline? Where are the intermediate results stored? And of course, what else can we achieve with ranges.
• First pipeline in detail: what happens at every step
• There are no loops: Lazy evaluation of the views
• Second pipeline in detail. Differences between Views and Actions
Everything you can do with loops and algorithms; you can also do with ranges. Thanks to lazy evaluation of views, the code becomes more functional, and often more compact.
• Returning ranges from functions
• Creating a new range that returns normally distributed random numbers
• Building a histogram with ranges
It’s easy to use multiple threads in modern C++. Writing safe multi-threaded code, on the other hand, is not easy. The difficult part is accessing data.
• Four modes of concurrent data access
• Example of race condition
• About std::thread and how to use it safely
Even something as simple as an integer is not immune from data races. Atomics can solve this problem. They are often implemented at hardware level, and C++ provides an API. In this video, we will learn how to use atomics.
• Introduction to std::atomic. Types that support atomic operations
• Implementing publication safety pattern with atomics
• Getting an extra bit of performance with correct memory order
Mutex is an object that lets us protect a resource from being accessed from multiple threads simultaneously. Mutex is one of many synchronization primitives. It’s easy to use, but there are a few caveats.
• Example of a mutex: Two threads updating a vector. Deadlocks
• How to avoid common problems by using RAII? Different kinds of locks in STL.
• Example: Using a mutex to protect a shared resource
Condition variables let threads notify each other that work is available or finished. This video provides an overview and an example of using condition variables to implement producer-consumer pattern.
• How condition variables work together with a mutex?
• Adding condition variable to solve a performance problem from the previous example
• Inspecting the results and identifying performance problems
C++17 has introduced parallel STL. Many of the existing algorithms can now be executed in parallel. Parallelism can be enabled by specifying execution policies. In this video we will learn how existing algorithms can be parallelized with execution policies, and also about the new algorithms.
• Execution policies and how to use them
• Unsequenced execution policies
• New parallel algorithms: Reduce, transform, inclusive, and exclusive scans
Thread pool is a collection of threads that execute incoming jobs or tasks. Clients push tasks on the queue, and threads pick them one at a time. This provides an abstraction for background tasks. In addition, there is a single point of control for how many threads are allowed in our program. And of course, we can create pipelines of tasks using fork/join paradigm.
• STL does not provide a thread pool. Here are some libraries to consider
• How does a thread pool work?
• Implementing our own thread pool
Publication safety pattern that we have implemented with atomics, is somewhat harder to implement for non-trivial types. Luckily, STL provides some primitives for publication safety. In this video we will learn about std future, std promise, and some related STL features.
• What is std::future and what you can do with it?
• Using std::async and why it’s not a good tool for parallel tasks
• Std::promise as “the other side” of the future.
In this video, we will use promise, future, and a lambda to create a version of std::async that uses our thread pool. Then we will learn some design patterns for efficient concurrent programming.
• Overview of the task class and related types
• Implementing task execution and publication of results
• Running our first asynchronous task
Continuation is when an asynchronous task consumes results of the previous one. In this video we will implement and use such pattern. Additionally, we will see how to handle exceptions and propagate them to the caller of a task.
• Adding continuation support to the task class
• Writing some usage code to define what we want from the interface
• Implementing the continuation task
Fork/Join builds on top of continuation. The idea is that not one, but multiple parallel tasks can consume results of the previous one. And after those tasks are finished, another task aggregates the results. In this video we will both use and implement such a feature.
• Using fork and join to create a pipeline of tasks
• How is fork/join implemented?
• Some useful tricks for manipulating tuples
Task creation is cheap, but it’s not free. We have to find a compromise between loading all threads with data and introducing unnecessary overhead. In this video we will learn a simple formula for distributing the work, and also discover some limitations of task-based parallelism, when implemented using mutexes.
• Overhead of tasks and the optimal way to partition data
• Implementing data partitioning
• Inspecting performance problems caused by blocked threads
Coroutine is a function whose execution can be suspended and resumed. Suspension saves the state of the function, then the caller can execute as if the function has returned. Resumption loads the saved state back and continues execution from where it was suspended. In this video we will learn about the possibilities of coroutines.
• Coroutines in a nutshell: suspension and resumption
• Different ways to wait for a lengthy operation to complete
• How coroutines solve problems with blocked execution?
In order to be able to implement coroutines, we have to understand a few low-level mechanisms. This video is an overview of how process memory is structured, how functions are called, and finally, how coroutine state is copied from the stack into the heap (and back).
• Process memory model: stack and heap
• How functions are called and what’s a function state?
• What happens when a coroutine is suspended and resumed
C++ does not specify semantics for coroutines. Instead, the language provides some low-level constructs. The promise is an interface for a coroutine’s state machine, and there are also a few primitives for suspending and resuming a coroutine. We get complete control over a coroutine’s behavior, but it’s not easy to get started. This video provides an overview of all parts that make coroutines work.
• What’s included? Operators, awaiters, handle, promise, coroutine_traits
• Understanding coroutine’s return type, promise type, and lifecycle
• Suspending and resuming a coroutine; returning values
Writing a coroutine framework is not as simple as writing a function. In this video, we will try to create the simplest coroutine possible. This coroutine will do nothing useful, but we will implement all the required elements.
• Writing the coroutine
• Implementing the promise type
• Inspecting the lifecycle of our coroutine
Our first coroutine was suspended, but never resumed. In this video we will see how to implement coroutine’s resumption for lazy evaluation, and also how to handle return values of a coroutine.
• Implementing co_return support in the promise
• Adding shared state to the coroutine result
• Using coroutine handle to resume a coroutine
We had implemented a concurrent tasks framework in Section 9. Back then we observed that threads had to wait on each other, which reduces concurrency. In this section, we will create a tasks framework that uses coroutines to achieve the maximum level of concurrency.
• What will we create, and what classes must be implemented?
• Overview of the thread pool
• Overview of the coroutine-based task classes.
Our task class should enable scheduling coroutines on thread pool threads. The coroutine must suspend, and then resume on one of those threads.
• Implementing task’s resumption on task manager thread
• Implementing task suspension with a custom awaiter
• Using our new framework and verifying that a coroutine resumes on the right thread
Our coroutines are now able to suspend and then to resume on executor threads. They cannot return values yet though. A coroutine returns the value using co_return operator, which forwards the value to promise_type::return_value. The value must then be communicated to the shared state.
• Implementing return_value in the promise
• Using std::promise and shared future to guarantee publication safety
• Running the application and inspecting debug output
We now can write coroutines that run on executor threads and produce values. Let’s see if we can make working with them a little bit more pleasant. We will also implement await_transform to allow co_awaiting a different type. Finally, we will find out whether the new framework solves the problem with threads being blocked by tasks that are waiting for each other.
• Removing the need for co_await by using initial_suspend
• Adding support for task names with await_transform
• Adding fork/join and investigating the block
We want to co_await other tasks. To implement this, every task will keep handles to coroutines that are waiting on it. Once a task is finished, it will cal resume on those handles. All resumptions should be scheduled with the executor.
• The executor-based resumer
• Adding an awaiter for the tasks
• Changing the coroutine to co_await sub-tasks
In this video we will see how useful the new coroutine framework is. We will take the example from Section 9, where we first calculate the average value, then use that value to find standard deviation and items above average in parallel. Then we will implement data partitioning and see that the new framework never blocks a thread while a task is waiting for another task to finish.
• Writing the chain of tasks, using lambdas as coroutines
• Adding data partitioning
• Inspecting the results and the level of parallelism
C++ Core Guidelines are mentioned many times throughout this course. The guidelines are written by some of the most experienced programmers in the world. They are not hard rules though, but rather recommendations and best practices, following which should make your programs safer and faster.
• Motivation for C++ Guidelines
• What is provided by the guidelines?
• Structure of the guidelines
Guidelines are, first and foremost, a collection of rules. If the rules are not enforced, they will not be followed. The guidelines are intended as specification for static code analysis tools. In this video we will learn how to enforce the guidelines using various technical methods.
• Static code analysis consists of parsing your code and finding potential problems
• Clang-tidy can be easily run from command line, and checks for many guidelines’ violations
• Guidelines support library includes helpers for enforcing the guidelines
Some guidelines cannot be enforced efficiently. They are still important, especially the guidelines concerning Philosophy, Architectural Ideas, and Non-Rules and myths.
• Express ideas directly in code, because compilers don’t read comments
• Write in ISO Standard C++
• Spread the word and introduce the best practices in your team
OpenCourser helps millions of learners each year. People visit us to learn workspace skills, ace their exams, and nurture their curiosity.
Our extensive catalog contains over 50,000 courses and twice as many books. Browse by search, by topic, or even by career interests. We'll match you to the right resources quickly.
Find this site helpful? Tell a friend about us.
We're supported by our community of learners. When you purchase or subscribe to courses and programs or purchase books, we may earn a commission from our partners.
Your purchases help us maintain our catalog and keep our servers humming without ads.
Thank you for supporting OpenCourser.