目录
当我们要定义的代码逻辑一样,但只是处理类型不一样时,模板的作用就体现出来了。使用模板可以极大的减少代码冗余、使代码简洁强大。
本文不赘述模板,只探讨一些需要注意的语法点。
1.非类型模板参数
在模板参数列表中,除了类型参数外,还可以使用非类型参数;但在实例化非类型参数时必须使用常量表达式。
例如下面这个对C风格数组求和的函数模板:
#include <iostream>
using namespace std;
template<typename T, size_t N>
T Sum(const T(&arr)[N]) {
T ret = 0;
for (T item : arr) {
ret += item;
}
return ret;
}
int main() {
double a[] = {4.4, 3.2, 7.3, 6.8, 5.0};
printf("%f\n", Sum(a)); // 特化出double Sum(double (&arr)[5]);
int b[] = {3, 1, 9, 3};
printf("%d\n", Sum(b)); //特化出int Sum(int (&arr)[4]);
return 0;
}
在此例中模板参数列表是一个类型参数T和一个非类型参数N,N的类型是size_t,表示数组大小。函数形参是【长度为N的T类型数组】的const引用。
该模板可以做到对任意基本类型、任意长度的C风格数组进行求和。
2.模板参数的推导
我们在使用函数模板的时候可以不指定类型参数,编译器会自动进行类型推导。
但编译器不会对类模板进行自动类型推导,因此在使用类模板时必须显示提供类型信息。
下面举一个函数模板的简单例子:求两个变量中的最大值
#include <string>
#include <cxxabi.h>
using namespace std;
#define GET_TYPENAME(expr) abi::__cxa_demangle(typeid(expr).name(), nullptr, nullptr, nullptr)
template<typename T>
const T &GetMax(const T &a, const T &b) {
printf("T = %s\n", GET_TYPENAME(T));
return a > b ? a : b;
}
template <typename T>
void f(T a, T b) {
printf("T = %s\n", GET_TYPENAME(T));
}
int main() {
string s1 = "abc";
const string s2 = "def";
f(s1, s2); //在拷贝时顶层const会被忽略
int a[3], b[4];
f(a, b); //数组会被转换为指针类型
printf("%d\n", GetMax(3, 5)); //编译器推导出T=int
printf("%d\n", GetMax<int>(3, 5)); //显示实例化T=int
printf("%f\n", GetMax(3.0, 5.0)); //编译器推导出T=double
printf("%f\n", GetMax<double>(3.0, 5.0)); //显示实例化T=double
// printf("%f\n", GetMax(3, 5.0)); //编译器报错,无法推导
printf("%f\n", GetMax<double>(3, 5.0)); //显示指定类型后编译器会对形参进行隐式类型转换
}
在不显示指定类型时,编译器会自动进行类型推导,且在必要时进行隐式类型转换。
但在不显示指定类型时编译器仅会进行最小范围的转换,包括数组可以转换为指针、拷贝时忽略顶层const、const引用绑定非const实参等。除此之外均不会自动进行隐式类型转换,因此需要调用者保证形参能够匹配上某个版本的模板,否则编译报错"no matching function for call to xxx"
3.尾置返回类型
当我们完全无法表示一个模板函数的返回值时,可以借助auto+尾置decltype来让编译器替我们做类型推导。
例如下面的例子,我们根本不知道T1类型和T2类型相加会返回什么类型。将decltype表达式放在函数的后面,再在前面用auto关键字来占位,可以解决此问题。
template<typename T1, typename T2>
auto add(T1 x, T2 y) -> decltype(x + y) {
return x + y;
}
4.对模板参数的类型转换
有时我们需要获取模板参数T的最原始类型,去掉其引用;有时又需要加上引用作为返回值。标准库<type_traits>提供了一组模板函数供我们使用:
返回值 | |
add_const<int>::type add_const<const int>::type add_const<int*>::type | const int const int int *const (对指针类型添加顶层const) |
remove_const<const int>::type remove_const<int *const>::type remove_const<const int*>::type | int int* (对指针类型只移除顶层const) const int* (不移除底层const) |
add_pointer<int>::type add_pointer<const int>::type add_pointer<int&>::type | int* const int* int* |
remove_pointer<int*>::type remove_pointer<int>::type remove_pointer<int**>::type | int int int* (只移除一层指针) |
add_lvalue_reference<int>::type add_lvalue_reference<int&>::type add_lvalue_reference<int&&>::type | int& int& (详见引用折叠规则) int& (详见引用折叠规则) |
add_rvalue_reference<int>::type add_rvalue_reference<int&>::type add_rvalue_reference<int&&>::type | int&& int& (详见引用折叠规则) int&& (详见引用折叠规则) |
remove_reference<int>::type remove_reference<int&>::type remove_reference<int&&>::type | int |
举个栗子,要编写一个函数模板,它返回迭代器所指向元素的拷贝。
但是当我们解引用迭代器的时候拿到的是引用类型,如果想返回它的拷贝,就要去掉这个引用,拿到它的原始类型作为函数返回值:
在此例中decltype(*it)是string&类型,remove_reference<decltype(*it)>::type是string类型。
#include <type_traits>
#include <vector>
#include <string>
using namespace std;
template<typename Iter>
auto returnCopy(Iter it) -> typename remove_reference<decltype(*it)>::type {
return *it;
}
int main() {
vector<string> strVec = {"hello", "world"};
string a = returnCopy(strVec.begin());
}
5.引用折叠规则
当我们间接(间接是指类型别名或模板参数)创建了“引用的引用”时,引用会发生折叠。折叠规则如下:
被折叠为: | |
左值引用的左值引用 (X&)& | 左值引用 X& |
左值引用的右值引用 (X&)&& | |
右值引用的左值引用 (X&&)& | |
右值引用的右值引用 (X&&)&& | 右值引用 X&& |
代码验证以上规则:
#include <iostream>
#include <type_traits>
using namespace std;
int main() {
using X = int&;
printf("%d\n", is_lvalue_reference<X&>::value);
printf("%d\n", is_lvalue_reference<X&&>::value);
using Y = int&&;
printf("%d\n", is_lvalue_reference<Y&>::value);
printf("%d\n", is_rvalue_reference<Y&&>::value);
}
6.模板参数引用的类型推导
当函数模板的参数是模板类型参数T的引用时,情况会变得比较复杂:
这张表格需要结合引用绑定规则(详见此链接2.1节)和引用折叠规则(上面的第5节)一起来理解。
以int为例 | 传入的实参是哪种值类别? | |||
函数形参x是模板类型参数T的哪种引用? | 非常量左值 | 常量左值 | 非常量右值 | 常量右值 |
非常量左值引用 template <typename T> | T被推导为int | T被推导为const int x的类型为const int& | × 不可传入 | × 不可传入 |
常量左值引用 template <typename T> | T被推导为int (const int& 能够绑定以上四种表达式类型) | |||
非常量右值引用 template <typename T> | T被推导为int& 折叠为int& | T被推导为const int& 折叠为const int& | T被推导为int x的类型为int&& | T被推导为const int x的类型为const int&& |
常量右值引用 template <typename T> | × 不可传入 | × 不可传入 | T被推导为int x的类型为const int&& |
引用折叠规则的存在,使得函数模板中的T&&成为了事实上的万能引用类型(注意此处要跟引用绑定规则中的万能引用类型const T&区分开)。
7.理解std::move
当有了前面的基础后,我们看看标准库提供的move函数是怎样将左值转换成右值的。move的源码如下:
#include <type_traits>
using namespace std;
template<typename T>
typename remove_reference<T>::type &&move(T &&x) noexcept {
return static_cast<typename remove_reference<T>::type &&>(x);
}
首先,函数形参x是模板参数T的右值引用(详见第6节表格倒数第2行),它可以绑定任意值类别的表达式。
- 当传入一个左值时,T被推导为左值引用,remove_reference将所有引用去除,暴露出原始类型,然后被static_cast强转成原始类型的右值引用,即左值被转换成了右值;
- 当传入一个右值时,T直接被推导为原始类型,remove_reference无效,然后被static_cast强转成原始类型的右值引用,右值依然保持右值属性。
8.完美转发
所谓完美转发(perfect forwarding),是指在函数模板中,完全按照模板实参的类型,将参数转发给另一个函数(保持左右值属性、const属性等)。
- 先看简陋版转发version1:
void funcImpl(int a) {
}
template<typename T>
void funcWrapper(T a) {
funcImpl(a);
}
每次转发还需要多拷贝一次参数,开销太大。我们应该用引用类型来避免拷贝。
- version2,用万能引用类型const T&:
void funcImpl(int a) {
}
template<typename T>
void funcWrapper(const T &a) {
funcImpl(a);
}
倒是左值右值都能传入了,但是funcImpl的形参不带const,编译不通过。
并且,如果funcImpl的形参是int&&,即它接受一个右值;此时右值被const T&绑定后变成了一个左值,丧失了其左右值属性,也不行。
- version3,用模板中的万能引用类型T&&:
void funcImpl(int a) {
}
template<typename T>
void funcWrapper(T &&x) {
funcImpl(static_cast<T &&>(x));
}
结合第6节表格倒数第2行来看:(以int类型举例)
- 如果传入一个非常量左值,T和x都被推导为int&,static_cast将x转换为(int&)&&,折叠为int&,依然是非常量左值;
- 如果传入一个常量左值,T和x都被推导为const int&,static_cast将x转换为(const int&)&&,折叠为const int&,依然是常量左值;
- 如果传入一个非常量右值,T被推导为int,x是int&&,static_cast将x转换为(int&&)&&,折叠为int&&,依然是非常量右值;
- 如果传入一个常量右值,T被推导为const int,x是const int&&,static_cast将x转换为(const int&&)&&,折叠为const int&&,依然是常量右值。
由此即完成了参数的完美转发。
C++标准库中已经封装好了这个转发函数供我们直接使用:std::forward。
有了它我们可以写一个万能的函数包装器,任意类型、任意参数个数、任意返回值的函数都可以用这个包装器来转发:
#include <utility>
#include <iostream>
#include <string>
using namespace std;
template<typename Fn, typename... Args>
auto FuncWrapper(Fn &&fun, Args &&...args) -> decltype(fun(forward<Args>(args)...)) {
return fun(forward<Args>(args)...);
}
void f1(int a, double &b) {
printf("f1\n");
}
char f2(string &&a) {
printf("f2\n");
}
int main() {
int a;
double b;
string c;
FuncWrapper(f1, a, b);
FuncWrapper(f2, move(c));
}
9.类模板
下面举一个类模板的例子,单例类:
template<typename T>
class Singleton {
public:
static T &GetInstance() {
static T instance;
return instance;
}
protected:
// 让子类能够访问到该类的构造和析构函数
Singleton() = default;
~Singleton() = default;
private:
// 禁用拷贝和移动
Singleton(const Singleton &) = default;
Singleton &operator=(const Singleton &) = default;
Singleton(Singleton &&) noexcept = default;
Singleton &operator=(Singleton &&) noexcept = default;
};
class A : public Singleton<A> {
public:
~A() = default;
private:
A() = default; //禁止外部构造该类
friend class Singleton<A>; //但允许Singleton<A>构造该类
};
int main() {
A &a = A::GetInstance();
}
通过将类A的构造函数放在private里,禁止外部实例化此类,同时将Singleton声明为友元,允许该友元构造A的实例,即GetInstance方法是该类的唯一访问入口。