Skip to content

模板、Concepts 与 C++20

模板实例化

模板定义本身是蓝图。编译器在某个具体类型真正被需要时生成对应代码,称为隐式实例化

cpp
template <typename T>
T add(T a, T b) { return a + b; }

int value = add(1, 2); // 隐式实例化 add<int>

模板定义通常放在头文件中,因为每个使用点都可能需要看到定义来完成隐式实例化。

对于少数固定、编译代价很高的类型,可在头文件声明 extern template,并在一个源文件中提供显式实例化定义:

cpp
// processor.hpp
template <typename T> class Processor { /* 完整定义 */ };
extern template class Processor<int>;

// processor.cpp
#include "processor.hpp"
template class Processor<int>; // 唯一的显式实例化定义

这样可减少重复实例化和目标代码膨胀,但其他未显式声明的类型仍需要完整模板定义可见。

SFINAE 与检测惯用法

SFINAE(Substitution Failure Is Not An Error)指模板参数替换失败时,编译器可丢弃该候选模板,而不是立即报错。C++17 可用 std::void_t 检测表达式是否有效:

cpp
#include <type_traits>
#include <utility>

template <typename T, typename = void>
struct has_size : std::false_type {};

template <typename T>
struct has_size<T, std::void_t<
    decltype(std::declval<const std::remove_reference_t<T>&>().size())
>> : std::true_type {};

template <typename T>
inline constexpr bool has_size_v = has_size<T>::value;

在转发引用模板中应先移除 T 的引用再检测成员,否则 T 可能是 std::vector<int>&,直接写 std::declval<T>().size() 会得到错误结果。

可变参模板与折叠表达式

C++17 折叠表达式简化了参数包计算:

cpp
template <typename... Ts>
auto sum(Ts... values) {
    return (values + ...); // 一元右折叠
}
cpp
template <typename... Ts>
std::string concat(Ts&&... values) {
    std::ostringstream out;
    (out << ... << std::forward<Ts>(values));
    return std::move(out).str();
}

上面的 concat 会在运行期构造 std::string不是编译期字符串拼接。只有参数和结果类型均支持常量求值时,constexpr 函数才可能在编译期执行;通用、可变长度的编译期字符串通常需要固定字符串(fixed string)类型或专门库。

C++20 Concepts

Concepts 用可读的约束替代大量 enable_if 与晦涩的模板报错:

cpp
#include <concepts>

template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;

template <Number T>
T multiply(T a, T b) {
    return a * b;
}

requires 表达式还能检查成员和结果约束:

cpp
template <typename T>
concept HasSize = requires (const T& value) {
    { value.size() } -> std::convertible_to<std::size_t>;
};

Concepts 主要改善接口约束、重载选择和诊断;它们不自动保证算法语义正确,也不替代运行期数据校验。

Ranges、容器与 View

容器拥有元素,如 std::vector;view 表示一个可遍历范围或适配器,常以惰性方式将 filter、transform、take 等操作组合起来:

cpp
auto first_ten_squares = values
    | std::views::filter([](int x) { return x % 2 == 0; })
    | std::views::transform([](int x) { return x * x; })
    | std::views::take(10);

遍历时才计算元素,因此可以避免构造中间容器,并能在 take(10) 后提前停止。不过不要把所有 view 简化为“绝不拥有数据”:多数 view 借用底层范围,views::all 等在某些右值场景可持有范围。无论哪种,都要理解其生命周期与迭代器失效规则。

filter_view 通常不满足 sized_range,所以 std::ranges::size(filter_view) 不一定可用;需要元素数量时可遍历计数或使用 std::ranges::distance。修改底层容器也可能使 view 的迭代器失效。

协程:句柄、promise 与 awaiter

C++20 协程是编译器将函数转换为可暂停/恢复状态机的语言机制,不是线程,也没有默认调度器或 I/O 运行时。

  • std::coroutine_handle<>:非拥有的控制句柄,可 resume()done()destroy()
  • promise_type:由返回类型定义,决定返回对象、初始/最终挂起、返回值和未处理异常的行为;
  • awaiter:co_await 的对象协议,提供 await_readyawait_suspendawait_resume,决定是否挂起、如何安排恢复以及恢复值;
  • 协程帧:保存局部状态、promise 等,通常动态分配,但优化后实现可不同。

协程返回对象必须清楚拥有协程帧,并在适当时机 destroy();异常应由 promise_type::unhandled_exception() 保存、传播或明确终止。实际使用优先采用成熟的 task/executor 库,避免手写句柄造成悬空恢复、重复销毁或泄漏。

使用 Markdown 与 VitePress 构建