OnceCallback Prerequisite Cheat Sheet: A Review of Core C++11/14/17 Features
Introduction
Let's be honest—this article isn't meant to teach you from scratch. If you're completely unfamiliar with concepts like move semantics and smart pointers, we recommend going back to Volume 2 and working through the relevant chapters before returning here. The role of this article is a cheat sheet: we pull out all the C++ features that the OnceCallback series will use repeatedly, and for each feature, we cover only three things—"what it is", "how to use it", and "where it appears in OnceCallback". The goal is to prevent you from getting stuck on a syntax detail when reading the upcoming articles.
Learning Objectives
- Quickly review all the fundamental C++11/14/17 features required for the OnceCallback series
- Understand the specific application of each feature within the OnceCallback design
- Establish the knowledge baseline needed for subsequent deep dives
Move Semantics and std::move
Move semantics are the foundation of the entire OnceCallback—it is a move-only type, and its core design relies entirely on move semantics. Let's quickly run through the core concepts.
Rvalue References and Move Constructors
C++11 introduced rvalue references T&&, which can bind to temporary objects (rvalues). The semantics of a move constructor T(T&& other) are to "steal" resources from other rather than making a copy. After the theft, other enters a "valid but unspecified" state—typically emptied out.
// 一个最简单的移动语义示例
class Buffer {
int* data_;
std::size_t size_;
public:
// 普通构造
Buffer(std::size_t n) : data_(new int[n]), size_(n) {}
// 移动构造:偷走 other 的资源
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // 清空源对象
other.size_ = 0;
}
~Buffer() { delete[] data_; }
};
Buffer a(100); // a 拥有 100 个 int
Buffer b = std::move(a); // b 偷走了 a 的资源,a 变空The Essence of std::move
std::move doesn't actually move anything—it is simply a static_cast<T&&> that unconditionally casts the passed object to an rvalue reference. The ones that actually perform the "move" are the move constructor or move assignment operator. The role of std::move is to tell the compiler, "I agree to treat this object as an rvalue; you may steal resources from it."
Application in OnceCallback
The way OnceCallback is invoked is std::move(cb).run(args...)—std::move converts cb into an rvalue, and run() detects this as an rvalue invocation via deducing this (a C++23 feature covered in a dedicated article later), executes the callback, and marks the state of cb as "consumed". Any subsequent access to cb is illegal. The entire design philosophy is: enforcing the "invalid-after-single-invocation" semantics through the type system.
OnceCallback also deletes its copy constructor and copy assignment operator (= delete), keeping only move operations. This means that at any given time, a OnceCallback object has only one owner—you cannot copy it; you can only transfer ownership via std::move.
Perfect Forwarding and std::forward
Perfect forwarding solves this problem: you write a function template that accepts parameters and passes them on to another function exactly as they are. "Exactly as they are" means preserving the value category (lvalue or rvalue) and const qualification of the parameters.
Forwarding References and Deduction Rules
When a function template's parameter is T&& and T is a template parameter, T&& is not a plain rvalue reference, but rather a forwarding reference (also known as a universal reference). The compiler deduces T based on the value category of the passed argument:
- Passing an lvalue
x(typeint) →T = int&,T&&collapses toint& - Passing an rvalue
42(typeint) →T = int,T&&is simplyint&&
The Role of std::forward
std::forward<T>(arg) decides whether to return an lvalue reference or an rvalue reference based on the type of the template parameter T:
template<typename T>
void wrapper(T&& arg) {
// std::forward 保持 arg 的原始值类别
target(std::forward<T>(arg));
}
int x = 10;
wrapper(x); // arg 是左值引用,forward 返回左值引用
wrapper(10); // arg 是右值引用,forward 返回右值引用If you don't use std::forward and pass arg directly, then arg inside the function is always an lvalue (because named variables are lvalues), and the rvalue information is lost.
Application in OnceCallback
Perfect forwarding appears many times in OnceCallback. The bind_once function template uses it to preserve the value category of the bound parameters—std::forward<BoundArgs>(args)... ensures that passed rvalues remain rvalues, and passed lvalues remain lvalues. The deducing this implementation of the run() method also uses std::forward<Self>(self) to perfectly forward the value category of self to the internal impl_run.
Variadic Templates and Parameter Pack Expansion
Variadic templates allow you to write functions or classes that accept an arbitrary number of arguments of arbitrary types. The template signature of OnceCallback, OnceCallback<R(Args...)>, uses a parameter pack.
Basic Syntax
template<typename... Types> // Types 是参数包
void print_all(Types... args) {
// args... 在这里展开
// sizeof...(Types) 返回参数数量
}Types... is called a parameter pack, which can contain zero or more types. args... is a function parameter pack, expanded at the call site. sizeof...(Types) is a compile-time constant that returns the number of elements in the pack.
Expansion Locations
Parameter packs can be expanded in multiple locations: function parameter lists, template parameter lists, initializer lists, capture lists (starting from C++20), and more. The most critical expansion location in OnceCallback is the lambda's capture list—a feature introduced only in C++20, which we will cover in a dedicated article later.
Application in OnceCallback
The Args... of OnceCallback<R(Args...)> is a parameter pack that appears repeatedly throughout the class's implementation—the parameter types of the constructor, the parameter types of run(), and the signature of the internal func_ all come from this pack. The BoundArgs... of bind_once is another parameter pack, expanded into the lambda's capture list and the call arguments of std::invoke.
Smart Pointer Cheat Sheet
OnceCallback internally uses two types of smart pointers; let's quickly go over their respective roles.
std::unique_ptr: Exclusive Ownership
unique_ptr is an exclusive smart pointer—only one unique_ptr can point to an object at any given time. It is not copyable, only movable. It is created using std::make_unique<T>(args...).
auto p = std::make_unique<int>(42);
// auto p2 = p; // 编译错误:不可拷贝
auto p3 = std::move(p); // OK:移动转移所有权
// 此后 p 为 nullptrIn OnceCallback, the significance of unique_ptr doesn't lie in us using it directly, but rather that OnceCallback must support lambdas that capture move-only objects—if a lambda captures a unique_ptr, then the std::move_only_function containing this lambda (OnceCallback's internal storage) must also be move-only. This is something std::function cannot achieve, and it is one of the reasons we chose std::move_only_function.
std::shared_ptr: Shared Ownership
shared_ptr manages an object's lifetime through reference counting. All shared_ptr instances pointing to the same object share a single reference count, and the object is destroyed when the last shared_ptr is destroyed.
auto p1 = std::make_shared<int>(42);
auto p2 = p1; // OK:拷贝,引用计数 +1
// p1 和 p2 都指向同一个 intIn OnceCallback, shared_ptr is used to manage the cancellation token CancelableToken. The token needs to be shared between the OnceCallback object and an external controller—the external controller calls invalidate() to invalidate the token, and OnceCallback checks the token's state via its own held shared_ptr copy before executing the callback. The reference counting of shared_ptr guarantees that as long as someone holds the token, the underlying Flag object will not be destroyed.
std::atomic and memory_order
The internal implementation of the cancellation token uses std::atomic<bool> and memory_order_acquire/release.
Atomic Operations
std::atomic<T> provides atomic access to variables of type T—reads and writes cannot be interrupted by other threads' operations. The basic operations are load() (read) and store() (write), which can specify a memory order.
std::atomic<bool> flag{true};
// 线程 A:写入
flag.store(false, std::memory_order_release);
// 线程 B:读取
if (flag.load(std::memory_order_acquire)) {
// flag 仍然为 true
}acquire/release Semantics
memory_order_release and memory_order_acquire are a pair of matching memory orders. Simply put: a release store guarantees that all writes prior to the store are visible to other threads; a acquire load guarantees that all reads after the load can see the writes preceding the release store. In OnceCallback's cancellation token, invalidate() uses a release store to set valid to false, and is_valid() uses a acquire load to read valid—this guarantees that if is_valid() returns true, all states related to the token are visible to the current thread.
enum class
enum class is the scoped enumeration introduced in C++11, solving the name pollution and implicit conversion problems of the old-style enum.
// 老式 enum:名字污染全局命名空间,可以隐式转成 int
enum Color { Red, Green, Blue };
int x = Red; // OK,隐式转换
// enum class:名字被限定在枚举作用域内,不可隐式转换
enum class Status : uint8_t {
kEmpty, // 从未被赋值
kValid, // 持有有效的可调用对象
kConsumed // 已被 run() 消费
};
Status s = Status::kValid;
// int y = s; // 编译错误:不可隐式转换OnceCallback uses enum class Status to distinguish between three states of the callback. Specifying the underlying type as uint8_t saves memory—the entire enumeration takes up only one byte.
Lambda Basics
Lambdas are everywhere in OnceCallback—constructing callbacks, bind_once, and the internal implementations of then() all rely on lambdas. Here is a quick review of the basic syntax.
auto add = [](int a, int b) { return a + b; };
// add 的类型是编译器生成的唯一闭包类
int x = 10;
// 值捕获:拷贝 x
auto f1 = [x]() { return x; };
// 引用捕获:引用 x(注意生命周期)
auto f2 = [&x]() { return x; };
// 初始化捕获(C++14):可以移动捕获
auto f3 = [p = std::make_unique<int>(42)]() { return *p; };The operator() of the closure class generated by a lambda is const by default—this means you cannot modify value-captured variables inside the lambda unless you add the mutable keyword. In the bind_once and then() implementations of OnceCallback, the lambda must be declared as mutable because it needs to call std::move(self).run() internally to modify the state of self. We will expand on this detail in the article on advanced lambda features.
Generic lambdas (starting from C++14) allow parameters to use auto:
auto generic = [](auto x, auto y) { return x + y; };
// 编译器为 operator() 生成模板版本The lambda inside bind_once uses (auto&&... call_args) to accept runtime arguments—here, auto&& is a forwarding reference (because auto is equivalent to a template parameter).
Type Traits
Type traits are tools for querying and manipulating type information at compile time. OnceCallback uses a few key traits; let's quickly go through them.
#include <type_traits>
// std::decay_t<T>:去掉 T 上的引用、const/volatile 限定符,数组变指针,函数变函数指针
using T1 = std::decay_t<const int&>; // T1 = int
using T2 = std::decay_t<OnceCallback&&>; // T2 = OnceCallback(去掉引用)
// std::is_same_v<A, B>:A 和 B 是否是同一类型
static_assert(std::is_same_v<int, int>); // 通过
static_assert(!std::is_same_v<int, double>); // 通过
// std::is_lvalue_reference_v<T>:T 是否是左值引用类型
static_assert(std::is_lvalue_reference_v<int&>); // 通过
static_assert(!std::is_lvalue_reference_v<int>); // 通过
static_assert(!std::is_lvalue_reference_v<int&&>); // 通过
// std::is_void_v<T>:T 是否是 void
static_assert(std::is_void_v<void>); // 通过
static_assert(!std::is_void_v<int>); // 通过In OnceCallback, std::decay_t and std::is_same_v are used in the not_the_same_t concept—it checks "whether the decayed template parameter is the same type as OnceCallback itself", used to prevent the template constructor from hijacking calls to the move constructor. std::is_lvalue_reference_v is used in the deducing this implementation of run()—it detects whether the caller passed an lvalue, and if so, triggers a static_assert error. std::is_void_v is used in impl_run() and then() to distinguish between void and non-void return types in compile-time branches.
if constexpr
if constexpr is the compile-time conditional branch introduced in C++17. The difference between it and a regular if is that the condition must be a compile-time constant expression, and the unselected branch is not compiled—not even syntax checking is performed. This feature is particularly useful when handling void return types.
template<typename R>
R do_something() {
if constexpr (std::is_void_v<R>) {
// void 返回:执行操作,不 return
perform_action();
return; // void return
} else {
// 非 void 返回:执行操作,return 结果
return perform_action();
}
}Without if constexpr and using a regular if, both branches would be compiled. At this point, the return result in the void branch would directly cause an error—void is not a type that can be assigned. if constexpr guarantees that the void case only generates the code for return;, and the non-void case only generates the code for return result;.
In OnceCallback, if constexpr (std::is_void_v<ReturnType>) appears in two places: the callback execution logic of impl_run(), and the chaining composition logic of then(). Both places deal with the same issue—void return types cannot be assigned and returned in the usual way.
decltype(auto)
decltype(auto) is the return type deduction method introduced in C++14. The difference between it and auto lies in the handling of references: auto discards references and top-level const, while decltype(auto) preserves them.
int x = 10;
int& ref = x;
auto f1() { return ref; } // 返回 int(丢掉了引用)
decltype(auto) f2() { return ref; } // 返回 int&(保留了引用)In OnceCallback, the lambdas in bind_once and then() use -> decltype(auto) as a trailing return type. The purpose of this is to perfectly forward the callable object's return value—if the called function returns int&&, decltype(auto) will also return int&&, without losing the value category information.
[[nodiscard]] Attribute
[[nodiscard]] is the attribute standardized in C++17, telling the compiler "the return value of this function should not be ignored". If the caller writes cb.is_cancelled(); but doesn't use the return value, the compiler will issue a warning.
[[nodiscard]] bool is_cancelled() const noexcept;
[[nodiscard]] bool maybe_valid() const noexcept;
[[nodiscard]] bool is_null() const noexcept;All three query methods of OnceCallback are annotated with [[nodiscard]]. The reason is simple—calling these methods is meant to get the return value for a check, and calls that ignore the return value are most likely typos (for example, writing if (!cb.is_cancelled()) as cb.is_cancelled();). The explicit of explicit operator bool() serves a similar purpose—preventing unintended behavior caused by implicit conversions to bool.
Ref-qualified Member Functions
C++11 allows non-static member functions to be ref-qualified, annotated with & or && after the function's parameter list. & means it can only be called on an lvalue, and && means it can only be called on an rvalue.
class Widget {
public:
void process() & {
// 只能通过左值调用:Widget w; w.process();
}
void process() && {
// 只能通过右值调用:Widget().process(); 或 std::move(w).process();
}
};In OnceCallback, the then() method is declared as auto then(Next&& next) &&—the trailing && means then() can only be called on an rvalue (via std::move(cb).then(next) or on a temporary object with .then(next)). This is another way to express consumption semantics—unlike run() which uses deducing this to differentiate between lvalues and rvalues to provide different error messages, then() doesn't need to distinguish between them, making the ref-qualifier approach more concise.
Summary
In this article, we quickly ran through all the fundamental C++ features that the OnceCallback series will use. For each feature, we clarified three points: what it is, how to use it, and where it will appear in OnceCallback. If you feel unfamiliar with any feature, we recommend going back to the corresponding chapter in the earlier volumes for a systematic study—upcoming articles will not re-explain these basic syntax elements.
Next, we are going to dive deep. The first stop is "Function Types and Template Partial Specialization"—this is the key to understanding the peculiar syntax of OnceCallback<R(Args...)>, and it is the entry point for building our entire template skeleton.