Chapter 20 of 24 | C++
Advanced language topics
Modern C++ provides powerful abstraction tools. Learn the concrete problem each feature solves before using it: templates support generic code, concepts constrain template interfaces, ranges compose sequence operations, and coroutines can express suspended computations.
Function and class templates
template <typename T>
T larger(T first, T second)
{
return first < second ? second : first;
}
A template describes code that can be instantiated for compatible types. The compiler checks the operations the template uses when it is instantiated. Template definitions usually live in headers because the definition must be visible at the point of instantiation.
Concepts express requirements
#include <concepts>
template <std::totally_ordered T>
T clamp_between(T value, T low, T high)
{
if (value < low) return low;
if (high < value) return high;
return value;
}
A concept names requirements on a type and can make template interfaces and diagnostics more understandable. Standard concepts are available in headers such as <concepts>. Constraints should describe what the implementation actually needs, not impose unrelated restrictions.
Ranges
C++20 ranges allow algorithms to work with ranges directly and compose views such as filtering and transformation. A view is often lazy and non-owning; it does not necessarily store transformed values. Ensure referenced data outlives the view, and avoid retaining iterators across changes that invalidate them.
#include <ranges>
#include <vector>
std::vector<int> values{1, 2, 3, 4, 5, 6};
auto even_squares = values
| std::views::filter([](int value) { return value % 2 == 0; })
| std::views::transform([](int value) { return value * value; });
Coroutines and other advanced features
C++20 coroutines allow a function to suspend and later resume, but the language does not provide one universal task or async runtime. A coroutine needs a promise type and a supporting library or framework. Use coroutines when their control-flow model benefits the design, and understand who owns the coroutine frame and how cancellation and errors are handled.
Other advanced topics include value categories, forwarding references, move semantics, constexpr evaluation, and compile-time metaprogramming. Approach them incrementally. Clear ordinary code is usually better than a clever template that only its author can maintain.
Templates are checked when used
A function template is a recipe for generating functions for particular types. When the compiler sees a call, it deduces template arguments from the call and instantiates a specialization. The operations used in the body must be valid for that type. This is why a template can appear to compile until a particular call tries to instantiate an unsupported operation.
Templates can reduce duplication, but generic code is only helpful if its contract is understandable. State what the type must support and what the function promises. Concepts make those requirements part of the declaration, improving diagnostics and documenting the intended set of types. Avoid writing a template when two small explicit functions are clearer.
Values, references, and moving
C++ distinguishes expressions that identify persistent objects from temporary results. This distinction influences overload resolution, copying, and moving. A move operation can transfer resources from an object that is about to be discarded rather than copying all of its contents. Standard containers and strings implement move operations, and return-by-value is normally the natural interface for a result.
std::move does not move an object by itself. It converts an expression into a form that permits a move operation to be selected. The move constructor or assignment operator performs the actual operation. After an object is moved from, it remains valid but its value is generally unspecified; use it only in ways allowed by its type, commonly by assigning a new value or letting it be destroyed.
Constexpr and compile-time work
constexpr marks functions and values that can participate in constant evaluation when their inputs and operations permit it. This can move calculations to compile time and let results be used where a constant expression is required. It does not mean every call is evaluated during compilation: a call with runtime input still runs at runtime.
Compile-time computation is useful for fixed tables, validation of constants, and generic utilities, but large metaprograms can increase build time and make diagnostics harder. Prefer ordinary runtime code unless compile-time evaluation provides a concrete benefit.
Ranges compose operations but need careful lifetimes
A ranges pipeline can describe “select these values, then transform them” as a composition. Many views defer work until iteration, so constructing the view may not perform the transformation at all. If the original container changes or dies, a view that references it may no longer be valid. Materialize the result into an owning container when it must outlive the source or be reused independently.
Ranges support many iterator types, but not every view is multipass or sized. Do not assume a view can always be traversed repeatedly or that computing its size is constant time. Check the range's guarantees when an algorithm depends on them.
Coroutines require a surrounding design
A coroutine can suspend at a defined point and continue later, which can make generators or asynchronous workflows easier to read. The language mechanism does not decide how tasks are scheduled, how errors propagate, how cancellation works, or which thread resumes execution. A supporting library or framework must supply these policies.
Coroutine frames can outlive the original call and store parameters and local state. Their ownership and destruction must be clear. Before adopting coroutines, identify who resumes and destroys each coroutine, what happens on cancellation, and how the framework reports failures.
Practice
- Write a template that returns the first value in a range satisfying a predicate, or use a standard algorithm that already does it.
- Add a concept constraint that makes the required operations explicit.
- Compose a filter and transform view, then explain its lifetime requirements.
- Read compiler diagnostics for a deliberately unsuitable template argument.
- Use
std::movewith a local string and explain which operation actually transfers its resources. - Compare a runtime calculation and a
constexprcalculation using compile-time and runtime inputs. - Turn a lazy view into an owning vector and explain why the copy is useful in that case.
Key idea: abstraction is useful when it clarifies a reusable contract. Prefer the simplest language feature that expresses that contract.
Template deduction and specialization
Template argument deduction compares a function parameter pattern with the type and value category of each argument. Reference qualifiers, cv-qualification, arrays, and function types affect the deduced result. Some conversions are not considered during deduction, and deduction rules differ for class templates and function templates. When a call fails, reduce it to one template and inspect the deduced types rather than adding casts blindly.
Class-template partial specialization can select an implementation for a family of types; function templates cannot be partially specialized, so overloads or constrained overloads are used instead. Constraints participate in overload ordering under defined rules. Keep specializations narrow and avoid overlapping conditions that make the chosen implementation difficult to predict.
Forwarding references and value categories
A forwarding reference is typically an rvalue reference to a deduced template parameter. With reference collapsing and std::forward, a wrapper can preserve whether its argument was an lvalue or rvalue. This enables perfect forwarding to constructors and callable objects, but it can also create large overload sets and surprising deduction.
Use forwarding only when the function is genuinely a transparent wrapper that should preserve the caller's value category. If a function consumes a value, a by-value parameter and move into storage may be simpler. If it only observes a value, a const reference or view may be clearer. Never forward the same named argument repeatedly as though it were unchanged; a forwarded rvalue may have been consumed by the first call.
Constant evaluation and compile-time constraints
Constant evaluation is governed by which operations are permitted in a constant expression and by the inputs available at compile time. consteval requires an immediate compile-time invocation in contexts where it applies, while constexpr allows compile-time evaluation when possible and runtime evaluation otherwise. These keywords encode different contracts.
Compile-time evaluation can validate fixed tables or produce values without runtime work, but template instantiation and constant evaluation consume compiler resources. A deeply recursive metaprogram can make diagnostics and builds slow. Modern loops and constexpr algorithms can be clearer than elaborate template recursion. Measure compile time as part of a large library's engineering cost.
Coroutines, schedulers, and cancellation
A coroutine's promise type determines how values, exceptions, and completion are communicated. Its handle controls resumption and destruction. The coroutine language machinery does not itself define a scheduler, executor, or standard universal task abstraction. Library design must decide who owns the frame, whether the coroutine starts immediately or lazily, what happens if a consumer abandons it, and how cancellation propagates.
Suspension can divide a function's execution across time and potentially across threads. Captured references must remain valid until resumption, and shared state must be synchronized if multiple threads access it. Model coroutine state transitions explicitly and use a mature framework when asynchronous correctness is important.
Advanced exercise
- Build a forwarding wrapper and test it with lvalues, const lvalues, and rvalues; explain each deduced type.
- Constrain a template with a concept and compare diagnostics against an unconstrained version.
- Measure compile time and binary size before and after a template-heavy abstraction.
- Document ownership, scheduling, cancellation, and exception behavior for a coroutine-based API.