模板类型推导
假设我们有如下的模板函数:1
2template <typename T>
void f(ParamType param);
一个该模板函数的调用语句可能如下所示:1
f(expr);
在编译阶段,编译器利用expr
来推导出两个类型:T
和ParamType
。ParamType
常带有变量的限定符,如const
和引用符号。
模板的类型推到不仅仅依赖于expr
的类型,还依赖于ParamType
的类型,ParamType
共分为三种类型:
- ParamType是一个指针或者引用(但不是universal reference, T&&)
- ParamType是一个universal reference
- ParamType既不是一个指针也不是一个引用
根据ParamType
的类型,模板的类型推到规则也有三种。
1. ParamType是一个指针或者引用(但不是一个universal reference)
这种情况下的推导规则为:
- 如果
expr
是一个引用,那么忽略expr
的引用 - 然后根据
expr
的类型和ParamType
的类型,推导出T
举几个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19template <typename T>
void f(T& param);
int x = 27;
const int cx = x; // cx is const int
const int& rx = x; // rx is const int&
f(x); // x is int => T is int, param's type is int&
f(cx); // cx is const int => T is const int, param's is const int&
f(rx); // rx is const int& => T is const int, param's type is const int&
template <typename T>
void f(T* param);
int x = 27;
const int *px = &x;
f(&x); // T is int, param's type is int*
f(px); // T is const int, param's type is const int*
2. ParamType是一个universal reference
一般来说函数模板有以下形式:1
2template <typename T>
void f(T&& param);
param
类型由以下规则得得出:
- T& + T& => T&
- T& + T&& => T&
- T&& + T& => T&
- T&& + T&& => T&&
举几个例子:1
2
3
4
5
6
7
8
9
10
11template <typename T>
void f(T&& param);
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // x is lvalue, T is int&, param's type is int&
f(cx); // cx is lvalue, T is const int&. param's type is const int&
f(rx); // rx is lval, T and param is const int&
f(27); // 27 is rvalue, T is int, param's type is int&&
3. ParamType既不是指针也不是引用
一般来说函数模板有以下形式:1
2template <typename T>
void f(T param);
此时,传递给函数的实参会被拷贝。此时函数模板的推导规则如下:
- 如果
expr
是引用,那么它的引用被忽略 - 之后,如果
expr
有const
限定符,那么const
被忽略,如果有volatile
限定符,volatile
被忽略。
还是例子:1
2
3
4
5
6
7int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T's and param's types are both int
f(cx); // T's and param's types are both int
f(rx); // ditto
注意到只是顶级const
被忽略,如果有这样一次函数调用:1
2const char* const ptr = "Fun with pointers";
f(ptr);
那么在函数中修改ptr所指向的字符串是不被允许的,而修改ptr
这个指针(如使其为nullptr)则是可以的。
数组参数
数组和指针是不同的类型,但是数组类型自动转换成指针类型,于是下面的代码是成立的:1
2const char name[] = "Test";
const char* ptrName = name;
在模板函数中,如果数组是有by-value传递给函数的,那么数组自动转换成指针,如果by-reference传递给函数,那么函数得到的是一个数组的引用。即:1
2
3
4
5
6
7
8
9
10template <typename T>
void f1(T param);
template <typename T>
void f2(T& param);
int arr[] = {1, 2, 3};
f1(arr); // T is int*
f2(arr); // T is int[], parameter's type is int (&)[]
于是我们可以用以下函数在编译期得到一个数组的大小:1
2
3
4template <typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept {
return N;
}
当然在C++11标准中,更推荐用std::array
来使用数组1
2int keyVals[] = {1, 2, 3};
std::array<int, arraySize(keyVals)> anotherArray;
函数参数
函数在C++中也是一种类型,而函数指针是一个指针类型,只是函数类型可以自动转换成函数指针。在函数模板中,函数的转换规则和数组类型相同。
举几个例子:1
2
3
4
5
6
7
8
9
10void someFunc(int, double);
template <typename T>
void f1(T param);
template <typename T>
void f2(T& param);
f1(someFunc); // param's type is void (*)(int, double)
f2(someFunc); // param's type is void (&)(int, double)
auto类型推导
auto
关键字是由C++11引入的,本质上auto
类型推导和模板类型推导是一样的,只要把auto
当作是T
,而变量的类型当作是模板参数类型,等号右侧当作是expr
即可。如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16auto x = 27; // auto -> T, auto -> paramType, so x is int
const auto cx = 27; // auto -> T, const auto -> paramType, so T is int, cx is const int
const auto& rx = x; // auto -> T, const auto& -> paramType, so T is int, rx is const int&
auto&& uref1 = x; // x is lvalue => uref1 is int&
auto&& uref2 = cx; // cx is lvalue => uref2 is const int&
auto&& uref3 = 28; // 28 is rvalue => uref3 is int&&
const char name[] = "Test";
auto arr1 = name; // arr1 is const char*
auto& arr2 = name; // arr2 is const char (&)[5]
void someFunc(int, double);
auto func1 = someFunc; // func1 is void (*)(int, double)
auto& func2 = someFunc; // func2 is void (&)(int, double)
唯一的不同是,对于auto,如果等号右边是大括号包围的列表,那么变量自动推导为std::initializer_list<T>
,而对于模板函数,传递一个{t1, t2, ..., tn}
给函数会导致编译错误。1
2
3
4
5
6
7
8
9
10
11auto t = {1, 2, 3}; // t's type is std::initializer_list<int>
template <typename T>
void f(T param);
f({1, 2, 3}); // error!
template <typename T>
void f_(std::initializer<T> il);
f_({1, 2, 3}); // correct
decltype类型推导
decltype
关键字用来得到表达式的类型。给几个例子:1
2
3
4
5
6
7
8
9
10
11const int i = 0; // decltype(i) is const int
bool f(const Widget& w); // decltype(f) is bool(const Widget&), function type
// decltype(w) is const Widget&
struct Point {
int x, y;
}; // decltype(Point::x) is int
// decltype(Point::y) is int
Widget w; // decltype(w) is Widget
if (f(w)) {...} // decltype(f(w)) is bool or can convert to bool
std::vector<int> v; // decltype(v) is std::vector<int>
于是我们可以写出以下函数:1
2
3
4
5
6template <typename T, typename Index>
auto authAndAccess(Container& c, Index i) -> decltype(c[i])
{
authUser();
return c[i];
}
operator[]
可能返回容器中的一个元素的引用或者返回容器中元素的拷贝,通过上述函数我们可以将这两种情况一同处理。decltype
有一个特殊情况需要注意,如果括号内是一个变量名,那么通常我们得到的是我们想要的类型,如果括号内是一个表达式,那么我们得到的是一个T&
,即1
2
3int x = 0;
// decltype(x) is int
// decltype((x)) is int&
boost::typeindex
boost
中的typeindex
模块可以在运行时得到正确的推导类型,一个例子如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29#include <iostream>
#include <vector>
#include <boost/type_index.hpp>
struct Widget {
int x;
};
template <typename T>
void f(const T& param) {
using std::cout;
using boost::typeindex::type_id_with_cvr;
cout << "T =\t"
<< type_id_with_cvr<T>().pretty_name()
<< '\n';
cout << "param =\t"
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
}
int main() {
std::vector<Widget> vw = {{1}, {2}, {3}};
const auto k = vw;
f(&k[0]);
return 0;
}
输出1
T = Widget const*
param = Widget const* const&