第26篇 模板函数:面试官让我手写一个通用swap,我差点翻车
上篇聊了友元和运算符重载,今天进入模板的世界。
模板是C++里最强大的特性之一,也是面试里区分候选人水平的分水岭。能把模板讲明白的人,C++基本不会差。
讲个面试场景。
面试官说:"写一个swap函数,交换两个变量的值。"
大部分人写:
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
面试官追问:"如果我要交换double呢?交换两个string呢?"
你总不能为每种类型都写一遍吧?
这时候就需要模板函数。
模板函数的基本语法
把刚才的swap改成模板版本:
template<typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
template<typename T>告诉编译器:这是一个模板,T是一个类型参数。编译器会根据你传入的实参类型,自动生成对应的函数。
int x = 1, y = 2;
swap(x, y); // 编译器生成 swap<int>
double a = 1.5, b = 2.5;
swap(a, b); // 编译器生成 swap<double>
string s1 = "hello", s2 = "world";
swap(s1, s2); // 编译器生成 swap<string>
这个过程叫模板实例化——编译器根据实际类型生成具体函数的过程。
模板在机器人开发里的实际应用
你可能会想:swap这种东西有什么实际意义?STL里已经有了。
对,STL的std::swap就是模板函数。但模板的应用远不止swap。
在机器人开发里,我们经常需要写通用的数学工具函数。比如求两个值中的较小值:
template<typename T>
T min_value(T a, T b) {
return (a < b) ? a : b;
}
// 用在不同的数据类型上
double min_speed = min_value(3.14, 2.71);
int min_iter = min_value(100, 200);
再比如一个通用的数组求和函数:
template<typename T, int N>
T array_sum(const T (&arr)[N]) {
T sum = 0;
for (int i = 0; i < N; i++) {
sum += arr[i];
}
return sum;
}
double lidar_data[] = {1.2, 3.4, 5.6, 7.8};
double total = array_sum(lidar_data); // N自动推导为4
模板让一套代码适配所有类型,这在写算法库的时候特别有用。
模板参数的推导
面试经常考的一个点:编译器怎么知道T是什么类型?
大多数情况下,编译器会自动推导。但有些时候推导会失败:
template<typename T>
void print(T value) {}
print(10); // T推导为int
print(3.14); // T推导为double
print("hello"); // T推导为const char*
但如果你这么写:
template<typename T>
void process(T a, T b) {}
process(10, 3.14); // 编译错误!T不能同时是int和double
两个参数的类型不一致,编译器不知道该推导成什么。解决办法是显式指定类型:
process<double>(10, 3.14); // 明确告诉编译器T是double,10会被提升为10.0
这个知识点面试偶尔会考。实际开发中也很常见——你以为编译器能推导,结果它报了一堆看不懂的错误信息。
模板特化:给特定类型"开小灶"
有时候,通用版本的模板对某些类型不适用,需要特殊处理。这就是模板特化。
// 通用版本
template<typename T>
void process(T value) {
cout << "Generic: " << value << endl;
}
// int的特化版本
template<>
void process<int>(int value) {
cout << "Int special: " << value * 2 << endl;
}
process(3.14); // 调用通用版本,输出: Generic: 3.14
process(42); // 调用特化版本,输出: Int special: 84
在机器人开发里,模板特化常用于优化。比如通用的矩阵乘法用三层循环,但对特定大小的矩阵(比如3x3、4x4),可以手写展开版本来提升性能。
非类型模板参数:模板不只是类型
刚才的array_sum里用到了int N,这就是非类型模板参数。模板参数不一定是类型,也可以是整数、指针、枚举等编译期常量。
template<typename T, int SIZE>
class FixedBuffer {
T data_[SIZE];
int count_ = 0;
public:
void push(const T& val) {
if (count_ < SIZE) data_[count_++] = val;
}
T get(int index) const { return data_[index]; }
int size() const { return count_; }
};
FixedBuffer<double, 100> lidar_buf; // 固定100个double的缓冲区
FixedBuffer<int, 50> status_buf; // 固定50个int的缓冲区
在机器人开发里,固定大小的缓冲区很常见——比如传感器数据环形缓冲、IMU采样队列。用非类型模板参数,大小在编译期就确定了,不用动态分配内存,性能比vector好。
这也是为什么STL的std::array是模板类——std::array<int, 5>里的5就是非类型模板参数。
面试中的模板陷阱
面试官考模板,有几个经典的坑。
第一个:模板函数不支持隐式类型转换。
template<typename T>
void print(T value) { cout << value; }
void func(int x) { cout << x; }
func(3.14); // 合法,double隐式转为int
print(3.14); // 合法,T推导为double
// 但如果:
template<typename T>
void add(T a, T b) { cout << a + b; }
add(1, 2.5); // 错误!T推导冲突
第二个:模板函数的定义必须放在头文件里。因为编译器在实例化的时候需要看到完整的定义,不是声明。如果你把模板函数放在.cpp里,链接的时候会报"未定义的引用"。
这个坑我当年踩了好几次。新手写模板,经常习惯性地把声明放.h、实现放.cpp,结果编译通过但链接失败。
第三个:模板不是虚函数。模板在编译期实例化,虚函数在运行期绑定,两者是不同的机制。不能把模板和虚函数混在一起用。
再补充一个面试加分话题:SFINAE和if constexpr。SFINAE(Substitution Failure Is Not An Error)是模板编程里的核心机制,意思是模板参数替换失败不是错误,编译器会尝试其他重载。你可以用std::enable_if来限制模板函数只对特定类型生效,比如写一个只对数值类型有效的average()函数。C++17之后更推荐用if constexpr来做编译期分支判断,代码写起来更直观。比如你可以在一个模板函数里用if constexpr (std::is_arithmetic_v<T>)来判断T是不是数值类型,不是的话编译期直接跳过那段代码。面试时能聊到if constexpr说明你了解现代C++特性,比只谈模板特化更有说服力。
给正在准备面试的你一点建议
模板函数在面试里考的不深,但一定要会手写。
最基础的:能写一个模板函数,理解类型推导,知道模板定义要放头文件。这三点做到了,面试就不会丢分。
进阶一点:知道模板特化是什么,能举出一个实际应用的例子。如果能讲到"STL的sort就是模板函数",面试官会觉得你对STL也有了解。
模板函数是模板类的基础。下篇会讲模板类——STL容器的底层设计思路。两者结合起来,你对模板的理解就完整了。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。
「机器人软件开发面试·从入门到精通」连载系列
上一篇:第25篇 友元与运算符重载——让自定义类型"好用"的秘密
下一篇预告:第27篇 模板类——STL容器的底层设计思路
有任何问题欢迎评论区留言,我会尽量回复。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)