一、Template(模板)解读
一、模板本质:不是泛型,是"代码生成器"
Template = 编译期函数 / 类型生成系统
cpp
template<typename T>
T add(T a, T b) { return a + b; }
编译期行为:
cpp
add<int> -> 生成一个 int 版本
add<double> -> 再生成一个 double 版本
关键点:
- 模板 ≠ 多态
- 模板在编译期展开
- 每个实例化是独立函数/类型
模板代码膨胀、编译慢的根本原因
二、模板参数的全部形态
1 类型模板参数(最常见)
cpp
template<typename T>
struct Box { T value; };
typename和class等价- 推荐统一用
typename
2 非类型模板参数(NTTP)
C++11 之前
cpp
template<int N>
struct Array {
int data[N];
};
C++17:auto NTTP
cpp
template<auto N>
struct Buffer {};
C++20:结构体作为 NTTP
cpp
struct Config {
int a;
int b;
};
template<Config C>
struct Foo {};
要求:
constexpr- 结构必须是 literal type
3 模板模板参数(高阶模板)
cpp
template<typename T, template<typename> class Container>
struct Wrapper {
Container<T> data;
};
使用:
cpp
Wrapper<int, std::vector> w;
非常适合写 STL 风格库
三、函数模板 vs 类模板(差异巨大)
函数模板
cpp
template<typename T>
void foo(T x);
特点:
- 支持模板参数推导
- 可重载
- 不支持偏特化
类模板
cpp
template<typename T>
struct Foo {};
特点:
- 支持偏特化
- 不能自动推导(C++17 CTAD 除外)
- 是元编程核心
四、模板特化:全特化 vs 偏特化(高频炸点)
1 全特化(函数 & 类都支持)
cpp
template<>
struct Foo<int> {};
函数:
cpp
template<>
void bar<int>(int x) {}
2 偏特化(只支持类模板)
cpp
template<typename T>
struct Foo<T*> {};
函数模板不支持偏特化
cpp
template<typename T>
void f(T);
template<typename T>
void f<T*>(T*); // 报错
解决方案:
- tag dispatch
- if constexpr
- concepts
五、模板实例化机制(编译错误的根源)
1 两阶段查找(Two-phase lookup)
cpp
template<typename T>
void f(T x) {
g(x); // g 何时查找?
}
- 第一阶段:语法检查
- 第二阶段:实例化时查找依赖名
这就是模板错误信息"鬼畜"的原因
2 SFINAE(替换失败不是错误)
cpp
template<typename T>
auto foo(T t) -> decltype(t.size(), void()) {}
- 替换失败 → 忽略该重载
- 不报错
模板"选择性可用"的基础
六、现代替代 SFINAE:if constexpr + Concepts
if constexpr(C++17)
cpp
template<typename T>
void print(const T& x) {
if constexpr (std::is_integral_v<T>) {
std::cout << "int\n";
} else {
std::cout << "other\n";
}
}
不满足的分支 不实例化
Concepts(C++20,模板的终极形态)
cpp
template<typename T>
concept Point = requires(T p) {
p.x;
p.y;
};
template<Point P>
void draw(P p) {}
好处:
- 错误信息极友好
- 接口即文档
- 可读性质变
七、模板元编程(Compile-time Programming)
1 类型计算(typelist)
cpp
template<typename... Ts>
struct TypeList {};
2 递归 vs 折叠表达式
递归(老派)
cpp
template<int N>
struct Factorial {
static constexpr int value = N * Factorial<N-1>::value;
};
折叠(C++17)
cpp
template<typename... Ts>
constexpr int sum(Ts... xs) {
return (xs + ...);
}
3 constexpr if + template = 编译期策略
cpp
template<typename T>
auto norm(const T& x) {
if constexpr (requires { x.norm(); }) {
return x.norm();
} else {
return std::abs(x);
}
}
八、模板与链接(ODR 地雷区)
为什么模板一般写在 .h?
实例化发生在使用点
cpp
template<typename T>
void foo(T);
foo(1); // 编译器此时才生成代码
.cpp 里看不到 → 链接失败
显式实例化(高级用法)
cpp
// header
template<typename T>
void foo(T);
// cpp
template void foo<int>(int);
控制代码膨胀
加快编译
九、模板设计黄金法则
1. 接口模板,内部具体化
cpp
template<typename T>
void api(T x) {
impl<T>(x);
}
2. 模板参数越少越好
模板是编译期耦合
3. 不要滥用模板表达"运行期差异"
错误
cpp
template<bool Debug>
void log();
正确
cpp
if constexpr (Debug)
4. STL 级模板要 Concepts
十、最常可能踩的坑
| 问题 | 原因 |
|---|---|
| C2766 | 显式特化重复定义 |
| C2765 | 显式实例化带默认参数 |
| 链接错误 | 模板定义不在 header |
| STL 性能差 | move ctor 缺 noexcept |
| 编译巨慢 | 模板层级过深 |
十一、模板 + Eigen / GTSAM / SLAM 的正确姿势
cpp
template<typename Scalar, int Dim>
using Vec = Eigen::Matrix<Scalar, Dim, 1>;
cpp
template<typename PointT>
concept EigenPoint = requires(PointT p) {
p.norm();
};
现代工程模板库几乎离不开 Concepts
十二、总结
模板不是"炫技工具",而是
- 编译期抽象
- 类型安全的代码生成
- 高性能库的基础设施
二、 模板报错如何"逆向阅读"(工程级方法论)
核心思想一句话 :
模板报错不是给看的,是给编译器看的要做的是:从"最后一个真正错误"逆推
一、模板报错的三层结构
典型模板错误(节选)
text
error: no matching function for call to 'foo(...)'
note: candidate template ignored: substitution failure [with T = ...]
note: in instantiation of function template specialization 'bar<T>'
note: in instantiation of class template 'Baz<T>'
note: required from here
三层含义
| 层级 | 你该看什么 |
|---|---|
| 第 1 层(最重要) | no matching function / invalid operands |
| 第 2 层 | substitution failure(SFINAE / concept 不满足) |
| 第 3 层 | required from here(实例化路径) |
99% 的时间只看第 1 层 + 最后一个 required from
二、逆向阅读模板错误的 5 步法(非常重要)
Step 1:直接滚到最底部
不要从头读
直接 滚到最后一个 required from here
text
required from 'foo<MyType>(...)'
这就是你的真实调用点
Step 2:锁定"第一次失败"的操作
找这种语句:
text
error: no member named 'norm' in 'MyType'
或
text
error: invalid operands to binary expression
这是"真实错误",不是模板噪音
Step 3:判断错误类别(快速分类)
| 错误特征 | 根因 |
|---|---|
no member named |
接口假设错误 |
invalid operands |
运算符未定义 |
no matching function |
模板约束不足 |
ambiguous |
偏特化 / 重载冲突 |
substitution failure |
SFINAE / Concepts |
Step 4:反推模板"隐含接口"
你必须问一句话:
"这个模板假设 T 一定具备什么?"
例如:
cpp
template<typename T>
auto f(const T& x) {
return x.norm();
}
隐含接口:
cpp
T::norm()
模板报错 ≠ bug
是 未文档化接口
Step 5:把错误"变成你自己的话"
原始报错:
text
invalid operands to binary expression
你的理解:
"我这个 T 没有定义 operator+"
能翻译成人话,说明你已经掌控了
三、3 个常遇到过的典型模板错误
1. C2766:显式特化重复定义
cpp
template<>
void transform_inplace(...) { ... }
template<>
void transform_inplace(...) { ... } //
逆向定位思路:
- MSVC 明确告诉你:
previous definition - 模板特化 就是普通函数
- ODR(One Definition Rule)违规
修复:只保留一个
2. C2765:显式实例化不能带默认参数
cpp
template void insert<PointCloud>(
const PointCloud&,
const Eigen::Isometry3d& pose = Eigen::Isometry3d::Identity() // ❌
);
逆向理解:
- 默认参数是 调用点语法糖
- 实例化是 实体定义
- 二者不能混
修复:
cpp
template void insert<PointCloud>(
const PointCloud&,
const Eigen::Isometry3d&
);
3. STL 容器退化(不是报错但很致命)
cpp
std::vector<MyType> v;
v.push_back(x); // copy instead of move
原因:
cpp
MyType(MyType&&) noexcept(false);
模板选择路径错误
不是 bug,是模板规则
四、模板报错调试神器(强烈建议)
| 工具 | 用途 |
|---|---|
static_assert(false, "...") |
定位实例化 |
typeid(T).name() |
快速看类型 |
clang++ -fconcepts-diagnostics-depth=3 |
概念报错 |
-ftemplate-backtrace-limit=0 |
完整路径 |
三、 Concepts + 数值库实战范式
目标 :
让模板"失败得体面"
把"鬼畜报错"变成"接口不满足"
一、数值库模板的三层设计模型(非常重要)
层 1:数学抽象(Concept)
cpp
template<typename T>
concept VectorLike = requires(T v) {
{ v.size() } -> std::convertible_to<int>;
{ v.norm() } -> std::convertible_to<double>;
};
层 2:算法模板
cpp
template<VectorLike V>
double squared_norm(const V& v) {
return v.norm() * v.norm();
}
层 3:具体类型适配
cpp
static_assert(VectorLike<Eigen::Vector3d>);
二、Eigen / SLAM 风格 Concept 模板(直接可用)
1. Eigen Vector
cpp
template<typename T>
concept EigenVector =
std::is_base_of_v<Eigen::MatrixBase<T>, T> &&
(T::ColsAtCompileTime == 1);
2. Lie Group(manif / Sophus 风格)
cpp
template<typename T>
concept LieGroup = requires(T x, typename T::Tangent v) {
{ T::Identity() } -> std::same_as<T>;
{ x.exp(v) } -> std::same_as<T>;
{ x.log() } -> std::same_as<typename T::Tangent>;
};
3. 点云点类型
cpp
template<typename P>
concept Point3D = requires(P p) {
{ p.x } -> std::convertible_to<double>;
{ p.y } -> std::convertible_to<double>;
{ p.z } -> std::convertible_to<double>;
};
三、Concepts 如何"终结模板地狱"
旧时代
cpp
template<typename T>
void align(const T& a) {
a.pose().inverse().matrix();
}
100 行报错
Concepts 时代
cpp
template<typename T>
concept PoseLike = requires(T t) {
{ t.pose() };
};
template<PoseLike T>
void align(const T& a) {
...
}
报错:
text
error: T does not satisfy PoseLike
这就是生产力提升
四、工程级模板规范
| 规则 | 理由 |
|---|---|
| 所有模板入口必须有 Concept | 防爆 |
| 算法模板 < 50 行 | 可维护 |
| 不在模板里写业务逻辑 | 编译慢 |
| 所有 NTTP 必须 constexpr | ABI 稳定 |
| Concepts > enable_if | 错误可读 |
五、给一个"SLAM 数值模板"的最小骨架
cpp
template<typename T>
concept Transform3D = requires(T t, Eigen::Vector3d p) {
{ t * p } -> std::same_as<Eigen::Vector3d>;
};
template<Transform3D T>
Eigen::Vector3d apply(const T& Tcw, const Eigen::Vector3d& p) {
return Tcw * p;
}
结语
模板能力的终点不是"写得多炫",而是:
- 报错是否人类可读
- 接口是否自解释
- 是否能在 6 个月后维护