编写时不指定具体类型,直到使用时才能确定,这个概念就是泛型。模板,顾名思义,编写一次即可适用于任意类型。模板定义以关键词 template 开始,后跟一个模板参数列表,类型参数前必须使用关键字 typename 或 class,在模板参数列表中这两个关键字含义相同,可以互换使用。函数模板通常不用声明为 inline,唯一例外的是特定类型的全特化,因为编译器可能忽略 inline,函数模板是否内联取决于编译器的优化策略
#include < cassert>
#include < string>
namespace jc {
template <typename T>
T max (const T& a, const T& b) {
return a < b ? b : a;
}
} // namespace jc
int main () {
assert (jc::max<int >(1 , 3 ) == 3 );
assert (jc::max<double >(1.0 , 3.14 ) == 3.14 );
std::string s1 = " down" ;
std::string s2 = " demo" ;
assert (jc::max<std::string>(s1, s2) == " down" );
}
两阶段编译(Two-Phase Translation)
模板编译分为实例化前检查和实例化两个阶段。实例化前检查模板代码本身,包括
检查语法是否正确,如是否遗漏分号
检查是否使用不依赖于模板参数的未知名称,如未声明的类型名、函数名
检查不依赖于模板参数的静态断言
template <typename T>
void f (T x) {
undeclared (); // 一阶段编译错误,未声明的函数
static_assert (sizeof (int ) > 10 ); // 一阶段,sizeof(int) <= 10,总会编译失败
}
int main () {}
实例化期间保证代码有效,比如对不能解引用的类型进行解引用就会实例化出错,此外会再次检查依赖于模板参数的部分
template <typename T>
void f (T x) {
undeclared (x); // 调用 undeclared(T) 才会出现函数未声明的实例化错误
static_assert (sizeof (T) > 10 ); // 如果 sizeof(T) <= 10 则实例化错误
}
int main () {
f (42 ); // 调用函数才会进行实例化,不调用则不会有实例化错误
}
调用模板时,如果不显式指定模板参数类型,则编译器会根据传入的实参推断模板参数类型
#include < cassert>
#include < string>
namespace jc {
template <typename T>
T max (const T& a, const T& b) {
return a < b ? b : a;
}
} // namespace jc
int main () {
assert (jc::max (1 , 3 ) == 3 ); // T 推断为 int
assert (jc::max (1.0 , 3.14 ) == 3.14 ); // T 推断为 double
std::string s1 = " down" ;
std::string s2 = " demo" ;
assert (jc::max (s1, s2) == " down" ); // T 推断为 std::string
}
实参的推断要求一致,其本身不会为了编译通过自动做类型转换
#include < cassert>
namespace jc {
template <typename T>
T max (const T& a, const T& b) {
return a < b ? b : a;
}
} // namespace jc
int main () {
jc::max (1 , 3.14 ); // 错误,T 分别推断出 int 和 double,类型不明确
}
字符串字面值传引用会推断为字符数组(传值则推断为 const char*
,数组和函数会 decay 为指针)
#include < cassert>
#include < string>
namespace jc {
template <typename T, typename U>
T max (const T& a, const U& b) {
return a < b ? b : a;
}
} // namespace jc
int main () {
std::string s = " down" ;
jc::max (" down" , s); // 错误,T 推断为 char[5] 和 std::string
}
对于推断不一致的情况,可以显式指定类型而不使用推断机制,或者强制转换实参为希望的类型使得推断结果一致
#include < cassert>
#include < string>
namespace jc {
template <typename T, typename U>
T max (const T& a, const U& b) {
return a < b ? b : a;
}
} // namespace jc
int main () {
std::string s = " demo" ;
assert (jc::max<std::string>(" down" , " demo" ) == " down" );
assert (jc::max (std::string{" down" }, s) == " down" );
}
也可以增加一个模板参数,这样每个实参的推断都是独立的,不会出现矛盾
#include < cassert>
namespace jc {
template <typename T, typename U>
T max (const T& a, const U& b) {
return a < b ? b : a;
}
} // namespace jc
int main () {
assert (jc::max (1 , 3.14 ) == 3 ); // T 推断为 int,返回值截断为 int
assert (jc::max<double >(1 , 3.14 ) == 3.14 );
}
#include < cassert>
namespace jc {
template <typename RT, typename T, typename U>
RT max (const T& a, const U& b) {
return a < b ? b : a;
}
} // namespace jc
int main () {
assert (jc::max<double >(1 , 3.14 ) == 3.14 );
assert ((jc::max<double , int , int >(1 , 3.14 ) == 3 ));
}
C++14 允许 auto 作为返回类型,它通过 return 语句推断返回类型,C++11 则需要额外指定尾置返回类型,对于三目运算符,其结果类型为两个操作数类型中更公用的类型,比如 int 和 double 的公用类型是 double
#include < cassert>
namespace jc {
template <typename T, typename U>
auto max (const T& a, const U& b) -> decltype(true ? a : b) {
return a < b ? b : a;
}
} // namespace jc
int main () { assert (jc::max (1 , 3.14 ) == 3.14 ); }
namespace jc {
template <typename T, typename U>
constexpr auto max (const T& a, const U& b) {
return a < b ? b : a;
}
} // namespace jc
int main () { static_assert (jc::max (1 , 3.14 ) == 3.14 ); }
#include < cassert>
#include < type_traits>
namespace jc {
template <typename T, typename U, typename RT = std::common_type_t <T, U>>
RT max (const T& a, const U& b) {
return a < b ? b : a;
}
} // namespace jc
int main () { assert (jc::max (1 , 3.14 ) == 3.14 ); }
#include < cassert>
namespace jc {
int f (int a, int b) { return 1 ; }
template <typename T, typename U>
int f (const T&, const U&) {
return 2 ;
}
} // namespace jc
int main () {
assert (jc::f (1 , 3 ) == 1 );
assert (jc::f<double >(1 , 3 ) == 2 );
assert (jc::f<>(1 , 3 ) == 2 );
assert (jc::f (1 , 3.14 ) == 2 );
assert (jc::f (3.14 , 1 ) == 2 );
}
模板参数不同就会构成重载,如果对于给定的实参能同时匹配两个模板,重载解析会优先匹配更特殊的模板,如果同样特殊则产生二义性错误
#include < cassert>
namespace jc {
template <typename T, typename U>
int f (const T&, const U&) {
return 1 ;
}
template <typename RT, typename T, typename U>
int f (const T& a, const U& b) {
return 2 ;
}
} // namespace jc
int main () {
assert (jc::f (1 , 3.14 ) == 1 );
assert (jc::f<double >(1 , 3.14 ) == 2 );
// jc::f<int>(1, 3.14); // 二义性错误
}
#include < cassert>
#include < cstring>
#include < string>
namespace jc {
template <typename T>
T max (T a, T b) {
return a < b ? b : a;
}
template <typename T>
T* max (T* a, T* b) {
return *a < *b ? b : a;
}
const char * max (const char * a, const char * b) {
return std::strcmp (a, b) < 0 ? b : a;
}
} // namespace jc
int main () {
int a = 1 ;
int b = 3 ;
assert (jc::max (a, b) == b);
assert (jc::max (&a, &b) == &b);
std::string s1 = " down" ;
std::string s2 = " demo" ;
assert (jc::max (s1, s2) == " down" );
assert (std::strcmp (jc::max (" down" , " demo" ), " down" ) == 0 );
}
namespace jc {
template <typename T>
const T& f (const char * s) {
return s;
}
} // namespace jc
int main () {
const char * s = " downdemo" ;
jc::f<const char *>(s); // 错误:返回临时对象的引用
}
#include < cstring>
namespace jc {
template <typename T>
const T& max (const T& a, const T& b) {
return b < a ? a : b;
}
// 新增函数来支持 C-style 参数
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("down", "de") 返回临时对象的引用
}
} // namespace jc
int main () {
const char * a = " down" ;
const char * b = " de" ;
const char * c = " mo" ;
jc::max<const char *>(a, b, c); // 错误:返回临时对象的引用
}
只有在函数调用前声明的重载才会被匹配,即使后续有更优先的匹配,由于不可见也会被忽略
#include < cassert>
namespace jc {
template <typename T>
int f (T) {
return 1 ;
}
template <typename T>
int g (T a) {
return f (a);
}
int f (int ) { return 2 ; }
} // namespace jc
int main () { assert (jc::g (0 ) == 1 ); }
用于原始数组与字符串字面值(string literal)的模板
字符串字面值传引用会推断为字符数组,为此需要为原始数组和字符串字面值提供特定处理的模板
#include < cassert>
#include < cstddef>
namespace jc {
template <typename T, typename U>
constexpr bool less (const T& a, const U& b) {
return a < b;
}
template <typename T, std::size_t M, std::size_t N>
constexpr bool less (T (&a)[M], T (&b)[N]) {
for (std::size_t i = 0 ; i < M && i < N; ++i) {
if (a[i] < b[i]) {
return true ;
}
if (b[i] < a[i]) {
return false ;
}
}
return M < N;
}
} // namespace jc
static_assert (jc::less(0 , 42 ));
static_assert (!jc::less(" down" , " demo" ));
static_assert (jc::less(" demo" , " down" ));
int main () {}
#include < cstddef>
namespace jc {
template <typename T>
struct A ;
template <typename T, std::size_t N>
struct A <T[N]> {
static constexpr int value = 1 ;
};
template <typename T, std::size_t N>
struct A <T (&)[N]> {
static constexpr int value = 2 ;
};
template <typename T>
struct A <T[]> {
static constexpr int value = 3 ;
};
template <typename T>
struct A <T (&)[]> {
static constexpr int value = 4 ;
};
template <typename T>
struct A <T*> {
static constexpr int value = 5 ;
};
template <typename T1, typename T2, typename T3>
constexpr void test (int a1[7 ], int a2[], int (&a3)[42], int (&x0)[], T1 x1,
T2& x2, T3&& x3) {
static_assert (A<decltype (a1)>::value == 5 ); // A<T*>
static_assert (A<decltype (a2)>::value == 5 ); // A<T*>
static_assert (A<decltype (a3)>::value == 2 ); // A<T(&)[N]>
static_assert (A<decltype (x0)>::value == 4 ); // A<T(&)[]>
static_assert (A<decltype (x1)>::value == 5 ); // A<T*>
static_assert (A<decltype (x2)>::value == 4 ); // A<T(&)[]>
static_assert (A<decltype (x3)>::value == 4 ); // A<T(&)[]>
}
} // namespace jc
int main () {
int a[42 ];
static_assert (jc::A<decltype (a)>::value == 1 );
extern int x[]; // 传引用时将变为 int(&)[]
static_assert (jc::A<decltype (x)>::value == 3 ); // A<T[]>
jc::test (a, a, a, x, x, x, x);
}
int x[] = {1 , 2 , 3 }; // 定义前置声明的数组
零初始化(Zero Initialization)
使用模板时常希望模板类型的变量已经用默认值初始化,但内置类型无法满足要求。解决方法是显式调用内置类型的默认构造函数
namespace jc {
template <typename T>
constexpr T default_value () {
T x{};
return x;
}
template <typename T>
struct DefaultValue {
constexpr DefaultValue () : value() {}
T value;
};
template <typename T>
struct DefaultValue2 {
T value{};
};
static_assert (default_value<bool >() == false );
static_assert (default_value<char >() == 0 );
static_assert (default_value<int >() == 0 );
static_assert (default_value<double >() == 0 );
static_assert (DefaultValue<bool >{}.value == false );
static_assert (DefaultValue<char >{}.value == 0 );
static_assert (DefaultValue<int >{}.value == 0 );
static_assert (DefaultValue<double >{}.value == 0 );
static_assert (DefaultValue2<bool >{}.value == false );
static_assert (DefaultValue2<char >{}.value == 0 );
static_assert (DefaultValue2<int >{}.value == 0 );
static_assert (DefaultValue2<double >{}.value == 0 );
} // namespace jc
int main () {}