函数模板示例
最简单的例子如下。使用作用域运算符(::)表示指定使用全局命名空间中的 max 模板,而非 std::max
#include <iostream>#include <string>template <typename T>T max(T a, T b) {return b < a ? a : b;}int main() {std::cout << ::max(1, 3); // 3std::cout << ::max(1.0, 3.14); // 3.14std::string s1 = "mathematics";std::string s2 = "math";std::cout << ::max(s1, s2); // mathematics}
Two-Phase Translation
- 模板被编译两次,分别发生在实例化前和实例化期间
- 实例化前检查模板代码本身,包括
- 检查语法是否正确,如是否遗漏分号
- 发现使用不依赖于模板参数的 unknown name(类型名、函数名……)
- 检查不依赖于模板参数的 static assertion
- 实例化期间,再次检查模板代码保证所有代码有效(如某个类型不支持
operator<),特别的是,会再次检查依赖于模板参数的部分
template <typename T>void f(T x) {undeclared(); // first-phase compile-time error if undeclared() unknownundeclared(x); // second-phase compile-time error if undeclared(T) unknownstatic_assert(sizeof(int) > 10, "int too small"); // always fails if sizeof(int) <= 10static_assert(sizeof(T) > 10, "T too small"); // fails if instantiated for T with size <= 10}
模板实参推断(Template Argument Deduction)
- 如果传递 int 给 const T&,T 会被推断为 int
```cpp
template
T max(const T& a, const T& b) { return b < a ? a : b; }
std::cout << ::max(1, 42); // int max
- 实参的推断**不允许自动类型转换**,要求每个 T 都要正确匹配。传值调用参数时,cv 限定符会被忽略,引用转换为对应的值类型,数组或函数转换为指针```cpptemplate <typename T>T max(T a, T b) {return b < a ? a : b;}int a = 1;const int b = 42;int& c = a;int arr[4];std::string s;::max(a, b); // OK:T 推断为 int::max(b, b); // OK:T 推断为 int::max(a, c); // OK:T 推断为 int::max(&a, arr); // OK:T 推断为 int*::max(1, 3.14); // 错误:T 推断为 int 和 double::max("hello", s); // 错误:T 推断为 const char[6] 和 std::string
两个参数类型不同时,有三个解决方案
- 强制转换参数类型:
max(static_cast<double>(1), 3.14) - 指定 T:
max<double>(1, 3.14) - 用两个模板参数指定不同类型
- 强制转换参数类型:
类型推断对默认实参无效:在圆括号的参数列表中指定T的默认实参是无效的
template <typename T>void f(T = "");f(1); // OK:T 推断为 int,调用 f<int>(1)f(); // 错误:不能推断 T
- 默认实参应该在尖括号的模板参数列表中声明
template <typename T = std::string>void f(T = "");f(); // OK
多模板参数(Multiple Template Parameters)
函数模板有两种参数,尖括号里的 T 叫模板参数(template parameter),参数列表里的 T 叫调用参数(call parameter),用来替换模板参数的各个对象叫模板实参,如 double
- 模板参数数量不限,但不能指定默认的模板实参(对于函数模板而非类模板),如对于上述问题,可以指定两个类型,但返回类型为 T 不一定符合调用者的意图
```cpp
template
T max(T a, U b) { return b < a ? a : b; }
auto m = ::max(1, 3.14); // 返回类型由第一个实参决定
- 模板实参不能推断返回类型,必须显式指定```cpptemplate <typename T, typename U, typename RT>RT max(T a, U b) { // RT 不能被推断出return b < a ? a : b;}::max(1, 3.14); // 错误::max<int, double, double>(1, 3.14); // OK:显式指定 RT 为 double
- 但前两个模板参数被推断,没必要显式指定,因此可以改变模板参数声明顺序,把 RT 放到最前,这样使用时只需要显式指定一个模板实参
template <typename RT, typename T, typename U>RT max(T a, U b) {return b < a ? a : b;}::max<double>(1, 3.14); // OK:返回类型为 double,返回 3.14
- C++14 允许 auto 作为返回类型,它通过 return 语句推断返回类型
template <typename T, typename U>auto max(T a, U b) {return b < a ? a : b;}
- 如果只支持 C++11,还需要指定尾置返回类型(有点像rust的函数返回值)
template <typename T, typename U>auto max(T a, U b) -> decltype(b < a ? a : b) {return b < a ? a : b;}
- 用 true 作为条件也一样,需要的只是推断类型而已
template <typename T, typename U>auto max(T a, U b) -> decltype(true ? a : b) {return b < a ? a : b;}
- 可以用 std::common_type 来获取不同类型中最通用的类型
#include <type_traits>template <typename T, typename U>typename std::common_type<T, U>::type max(T a, U b) {return b < a ? a : b;}//C++14 中能简写为#include <type_traits>template <typename T, typename U>std::common_type_t<T, U> max(T a, U b) {return b < a ? a : b;}
- 对于较为复杂的类型,可以写到一个模板参数中
#include <type_traits>template <typename T, typename U, typename RT = std::common_type_t<T, U>>RT max(T a, U b) {return b < a ? a : b;}
- 有时 T 必须是一个引用,但返回类型是引用类型时可能出现问题,此时可以利用 std::decay去掉引用修饰符、cv 限定符等描述符,退化到最基本的类型
#include <type_traits>template<typename T, typename U, typename RT = std::decay_t<decltype(true ? T() : U())>>RT max(T a, U b) {return b < a ? a : b;}//调用时即可不指定模板参数auto a = ::max(1, 3.14);
函数模板的重载
int max(int a, int b) { return b < a ? a : b; }template <typename T>T max(T a, T b) {return b < a ? a : b;}::max(1, 42); // 调用函数::max('a', 3.14); // 调用函数::max(1.0, 3.14); // 通过推断调用 max<double>::max('a', 'b'); // 通过推断调用 max<char>::max<>(1, 42); // 通过推断调用 max<int>::max<double>(1, 42); // 调用 max<double>
- 返回类型的重载可能导致调用的歧义
template <typename T, typename U>auto max(T a, U b) {return b < a ? a : b;}template <typename RT, typename T, typename U>RT max(T a, U b) {return b < a ? a : b;}auto a = ::max(1, 3.14); // 调用第一个模板auto b = ::max<long double>(3.14, 1); // 调用第二个模板auto c = ::max<int>(1, 3.14);// 错误:两个模板都匹配// 模板一为double max<int, double>, 模板二为int max<int, int, double>
- 指针或传统 C 字符串的重载
#include <cstring>#include <string>template <typename T>T max(T a, T b) { // 1 基本类型return b < a ? a : b;}template <typename T>T* max(T* a, T* b) { // 2 类型指针return *b < *a ? a : b;}const char* max(const char* a, const char* b) { // 3 char*return std::strcmp(b, a) < 0 ? a : b;}int main() {int a = 1;int b = 42;::max(a, b); // 调用 1,max<int>::max(&a, &b); // 调用 2,max<int>std::string s1 = "hello";std::string s2 = "world";::max(s1, s2); // 调用 1,max<std::string>const char* x = "hello";const char* y = "world";::max(x, y); // 调用 3}
- 如果用传引用实现模板,再用传值重载 C 字符串版本,不能用三个实参版本计算三个 C 字符串的最大值
#include <cstring>template <typename T>const T& max(const T& a, const T& b) {return b < a ? a : b;}const char* max(const char* a, const char* b) {return std::strcmp(a, b) < 0 ? b : a;}template <typename T>const T& max(const T& a, const T& b, const T& c) {return max(max(a, b), c); // 如果传值调用 max(a, b) 则出错}int main() {const char* a = "frederic";const char* b = "anica";const char* c = "lucas";::max(a, b, c); // run-time ERROR}
错误原因是
max(max(a, b), c)中,**max(a, b)**产生了一个临时对象的引用,这个引用在计算完就马上失效了重载版本必须在函数调用前声明才可见 ```cpp
include
template
template
int max(int a, int b) { //该函数需要放在模板函数前面才能被调用到 std::cout << 2; return b < a ? a : b; }
int main() { ::max(3, 1, 2); // 11 }
<a name="1bbbb204"></a>## 注意事项- 在模板中**通常传值更好,这样语法简单,编译器优化更好,移动语义可以使拷贝更廉价**(有时完全没有拷贝或移动),而且模板可能同时被简单和复杂类型使用,为简单类型选择复杂类型可能适得其反。尽管传字符串字面值常量或者原始数组容易出问题,但给它们传引用通常会造成更大的问题- 通常函数模板不需要声明为inline,唯一例外的是特定类型的全特化。因为编译器可能忽略 inline,函数模板是否内联取决于编译器的优化策略- C++11 可以使用 constexpr 函数来生成编译期值```cpptemplate <typename T, typename U>constexpr auto max(T a, U b) {return b < a ? a : b;}int a[::max(sizeof(char), 1000u)];std::array<int, ::max(sizeof(char), 1000u)> b;
