函数模板示例
- 最简单的例子如下。使用作用域运算符(::)表示指定使用全局命名空间中的 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); // 3
std::cout << ::max(1.0, 3.14); // 3.14
std::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() unknown
undeclared(x); // second-phase compile-time error if undeclared(T) unknown
static_assert(sizeof(int) > 10,
"int too small"); // always fails if sizeof(int) <= 10
static_assert(sizeof(T) > 10,
"T too small"); // fails if instantiated for T with size <= 10
}
模板实参推断(Template Argument Deduction)
- 如果传递 int 给 const T&,T 会被推断为 int
template <typename T>
T max(const T& a, const T& b) {
return b < a ? a : b;
}
std::cout << ::max(1, 42); // int max<int>(const int&, const int&)
- 实参的推断不允许自动类型转换,要求每个 T 都要正确匹配。传值调用参数时,cv 限定符会被忽略,引用转换为对应的值类型,数组或函数转换为指针
template <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 不一定符合调用者的意图
template <typename T, typename U>
T max(T a, U b) {
return b < a ? a : b;
}
auto m = ::max(1, 3.14); // 返回类型由第一个实参决定
template <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;
}
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;
}
#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;
}
#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, double>
#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
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)
产生了一个临时对象的引用,这个引用在计算完就马上失效了
- 重载版本必须在函数调用前声明才可见
#include <iostream>
template <typename T>
T max(T a, T b) {
std::cout << 1;
return b < a ? a : b;
}
template <typename T>
T max(T a, T b, T c) {
return max(max(a, b), c); // 即使对int也使用上述模板,因为没看见下面版本
}
int max(int a, int b) {
std::cout << 2;
return b < a ? a : b;
}
int main() {
::max(3, 1, 2); // 11
}
注意事项
- 在模板中通常传值更好,这样语法简单,编译器优化更好,移动语义可以使拷贝更廉价(有时完全没有拷贝或移动),而且模板可能同时被简单和复杂类型使用,为简单类型选择复杂类型可能适得其反。尽管传字符串字面值常量或者原始数组容易出问题,但给它们传引用通常会造成更大的问题
- 通常函数模板不需要声明为 inline,唯一例外的是特定类型的全特化。因为编译器可能忽略 inline,函数模板是否内联取决于编译器的优化策略
- C++11 可以使用 constexpr 函数来生成编译期值
template <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;
网友评论