模板参数推导
函数模板类型推导
简单的函数模板:
1
2
| template <typename T>
void f(ParamType param);
|
调用时:
在编译期间,编译器会推导两个类型:一个是 T ,另一个是 ParamType ,比如:
1
2
3
4
5
6
| template <typename T>
void f(const T& param);
// 调用
int x = 0;
f(x);
|
T 推导为 int
,ParamType 推导为 const inst&
,T 的推导不仅取决于 expr 的类型,还取决于 ParamType 的类型。
ParamType 有三种情况:
- ParamType 是一个指针或引用但不是万能引用(&&)
- ParamType 是一个万能引用
- ParamType 既不是指针也不是引用
ParamType 是一个指针或引用但不是万能引用(&&)
推导规则:
- 如果 expr 的类型是一个引用,忽略引用部分。
- 然后剩下的部分决定 T ,然后 T 与形参匹配得出最终 ParamType 。
例子:
1
2
3
4
5
6
7
8
9
10
| template <typename T>
void f(T& param); // param 是一个引用
int x = 10; // x 是 int
const int cx = x; // cx 是 const int
const int& rx = cx; // rx 是指向 const int 的引用
f(x); // T 是 int ,ParamType 是 int&
f(cx); // T 是 const int ,ParamType 是 const int&
f(rx); // T 是 const int ,ParamType 是 const int&
|
指针同理
ParamType 是一个万能引用
推导规则:
- 如果 expr 是左值,T 和 ParamType 都会被推导为左值引用。
- 如果 expr 为右值,使用上面的推导规则。
例子:
1
2
3
4
5
6
7
8
9
10
11
| template <typename T>
void f(T&& param); // param 是万能引用类型
int x = 10; // x 是 int
const int cx = x; // cx 是 const int
const int& rx = cx; // rx 是指向 const int 的引用
f(x); // x 是左值,所以T 是 int& ,ParamType 也是 int&
f(cx); // cx 是左值,所以T 是 const int& ,ParamType 也是 const int&
f(rx); // rx 是左值,所以T 是 const int& ,ParamType 也是 const int&
f(10); // 10 是右值,所以 T 是 int ,ParamType 就是 int&&
|
ParamType 既不是指针也不是引用
推导规则:
- 如果 expr 是一个引用,忽略引用部分。
- 如果忽略引用后 expr 是一个 const 或 volatile ,那么再忽略之。
例子:
1
2
3
4
5
6
7
8
9
10
11
12
| template <typename T>
void f(T param); // param 是值类型
int x = 10; // x 是 int
const int cx = x; // cx 是 const int
const int& rx = cx; // rx 是指向 const int 的引用
const char* const ptr = "fun"; // ptr 是一个常量指针,指向常量对象
f(x); // T 是 int ,ParamType 也是 int
f(cx); // T 是 int ,ParamType 也是 int
f(rx); // T 是 int ,ParamType 也是 int
f(ptr); // T 是 const char* , ParamType 也是 const char *
|
数组和函数实参
在很多上下文中,数组会退化为指向它一个元素的指针:
1
2
| const char name[] = "Hello"; // name 的类型是 const char[6]
const char* ptrToName = name; // 数组退化为指针
|
将数组类型传值给模板,会推导出一个指针类型,因为函数的数组声明会被视为指针声明,即 void f(int param[]);
等价于 void f(int* param);
。
1
2
3
4
| template <typename T>
void f(T param);
f(name); // T 是 const char*
|
但是函数可以接受指向数组的引用,所以如果 ParamType 为引用,T 会被推导为真正的数组类型。
1
2
3
4
| template <typename T>
void f(T& param);
f(name); // T 是 const char[13] ,ParamType 是 const char(&)[13]
|
函数类型和数组类似,也会退化为一个函数指针。
1
2
3
4
5
6
7
8
9
10
| void func(int, double); // func 类型是 void(int, double)
template <typename T>
void f1(T param);
template <typename T>
void f2(T& param);
f1(func); // T 是 void(*)(int, double)
f2(func); // T 是 void(int, double) ,ParamType 是 void(&)(int, double)
|
std::initializer_list 实参
模板参数为 T 时,无法推导;为 std::initializer_list<T>
或 T const(&)[N]
时才能推导:
1
2
3
4
5
6
7
8
9
10
11
12
| template <typename T>
void f1(T param);
template <typename T>
void f2(std::initializer_list<T> param);
template <typename T, int N>
void f3(T const(¶m)[N]);
f1({1, 2, 3}); // error, 无法推导出 T
f2({1, 2,3}; // T 为 int ,ParamType 为 std::initializer_list<int>
f3({1, 2, 3}); // T 为 int ,ParamType 为 const int[3]
|
auto 自动类型推导
auto 类型推导和函数模板类型推导规则基本一致,除了 std::initializer_list 这个例外情况。
当一个变量使用 auto 进行声明时,auto 扮演模板的角色,变量的类型说明符扮演 ParamType 的角色,下面是理想化的推导过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| auto x = 27;
template <typename T>
void fx(T param);
fx(27); // 推导出 T 是 int ,ParamType 为 int ,即 x 为int 。
const auto cx = x;
template <typename T>
void fcx(const T param);
fcx(27); // 推导出 T 是 int ,ParamType 为 const int ,即 cx 为const int 。
const auto& rx = cx;
template <typename T>
void frx(const T& param);
frx(27); // 推导出 T 是 int ,ParamType 为 const int& ,即 cx 为const int& 。
|
auto 类型说明符的三种情况:
- 类型说明符是一个个指针或引用但不是万能引用
- 类型说明符是一个万能引用
- 类型说明符既不是指针也不是引用
1
2
3
4
5
| auto x = 10; // auto 为 int ,x 为 int
const auto& rx = x; // auto 为 int ,rx 为 const int&
auto&& lv = x; // auto 为 int& ,lv 为 int& && 折叠为 int&
auto&& rv = 10; // auto 为 int ,rv 为 int&&
|
数组和函数一样:
1
2
3
4
5
6
7
| const char name[] = "hello";
auto arr1 = name; // arr1 是 const char*
auto& arr2 = name; // arr2 是 const char (&)[6];
void func(int, double);
auto func1 = func; // func1 是 void(int, double)
auto& func2 = func; // func2 是 void (&)(int, double)
|
auto 统一初始化推导
C++11 引入了统一初始化(uniform initialization)的语法:
- 直接初始化:
T object {arg1, arg2, ...};
- 拷贝初始化:
T object {arg1, arg2, ...};
在 C++17 之前,auto 统一初始化的类型都被推导为 std::initializer_list
1
2
3
4
| auto a = {10}; // std::initializer_list<int>
auto b {10}; // std::initializer_list<int>
auto c = {1, 2}; // std::initializer_list<int>
auto d {1, 2}; // std::initializer_list<int>
|
C++17 改变了规则:
- 拷贝初始化如果所有参数类型相同,推导为
std::initializer_list<T>
- 直接初始化如果只有一个参数,推导为 T ,如果有多个参数,推导失败
1
2
3
4
| auto a = {10}; // std::initializer_list<int>
auto b {10}; // int
auto c = {1, 2}; // std::initializer_list<int>
auto d {1, 2}; // c++ 17 错误,太多参数
|
注意: 在用比较新版本的编译器时,=auto d {1, 2}= 用 c++11 编译也会出错,应该是编译器做了限制,gcc 用 -fpermissive
选项可以编译成功。
C++14 允许 auto 用于函数返回值和 lambda 函数的形参,使用的机制是模板类型推导,所以无法从直接初始化列表推导。
1
2
3
4
5
6
7
| auto func() {
return {1, 2, 3}; // 错误,推导失败
}
std::vector<int> v;
auto reset = [&v](const auto& newV) { v = newV; };
reset({1, 2, 3}); // 错误,推导失败
|
auto 的优点
- 使用 auto 声明变量必须初始化,能够避免使用未初始化的变量
1
2
3
| int x; // 未初始化的变量
auto x2; // 错误,必须初始化
auto x3 = 0;
|
- auto 能够表示只有编译器才知道的类型,比如声明 lambda 函数
1
2
3
4
5
6
7
| auto f = [const auto& p1, const auto& p2] { return *p1 < *p2; };
// 没有 auto 的写法,用 std::function
std::function<bool(const std::unique_ptr<Widget> &p1,const std::unique_ptr<Widget> &p2)>
dereUPLess =
[](const std::unique_ptr<Widget> &p1,const std::unique_ptr<Widget> &p2){return *p1<*p2;};
|
- auto 可以解决依赖机器的类型问题
1
2
3
4
| std::vector<int> v;
auto sz = v.size(); // size 在 32 bit 和 64 bit 上大小不同
std::vector<int>::size_type sz = v.size(); // 原来的写法
|
- auto 可以避免意识不到的类型不匹配错误
1
2
3
4
5
6
7
| std::unordered_map<std::string, int>;
// 错误,std::unordered_map 的 key 是一个常量,std::pair 的类型为 std::pair<const std::string, int>
for (const std::pair<std::string, int>& p: m) {}
// auto 可以避免这些问题
for (const auto& p: m) {}
|
auto 的陷阱
auto 有时推导出的类型并不是我们需要的类型,常见于使用代理类并具有隐式类型转换
1
2
3
4
5
| std::vector<bool> features(const Widget& w); // bool 表示 Widget 是否有对应的 feature
// 是否具有高优先级
bool highPriority = features(w)[5];
processWidget(w, highPriority);
|
如果使用 auto ,就会导致未定义行为
1
2
| auto highPriority = features(w)[5];
processWidget(w, highPriority);
|
原因是 std::vector<bool> 的 operator[] 返回一个代理类 std::vector<bool>::reference 来扮演 bool& ,同时它能够隐式转换成 bool 。使用 auto 导致 highPriority 的类型为 std::vector<bool>::reference ,它没有第五 bit 的值,所以这个值取决于 std::vector<bool>::reference 的具体实现。
解决方法是直接使用 bool 或者使用 auto 但是进行一次显示转换
1
| auto highPriority = static_case<bool>(features(w)[5]);
|
使用 decltype
decltype 返回名字或者表达式的类型。
1
2
3
4
5
6
7
8
9
10
11
12
| const i = 0; // decltype(i) 是 const int
bool f(const Widget& w);
// decltype(w) 是 const Widget&
// decltype(f) 是 bool(const Widget&)
struct Point {
int x;
int y;
}
// decltype(Point::x) 是 int
// decltype(Point::y) 是 int
|
decltype 主要用于函数模板返回类型,而这个返回类型依赖形参
1
2
3
4
5
| template<typename Container,typename Index>
auto authAndAccess(Container&& c,Index i)->decltype(std::forward<Container>(c)[i]) {
authenticateUser();
return std::forward<Container>(c)[i];
}
|
C++14 支持 decltype(auto) ,上面可以优化一下
1
2
3
4
5
| template<typename Container,typename Index>
decltype(auto) authAndAccess(Container&& c,Index i) {
authenticateUser();
return std::forward<Container>(c)[i];
}
|
对于一个名字,decltype 将会产生这个名字被声明的类型。如果一个左值表达式除了名字还有类型,
decltype 会产生 T& 。
decltype (x) 是 int ,但是 decltype((x)) 是 int& ,因为 (x) 是一个比名字更复杂的左值表达式。
使用 decltype(auto) 时,加上括号就会导致类型不同。
1
2
3
4
5
6
7
8
9
| decltype(auto) f1() {
int x = 0;
return x; // decltype(x) 是 int ,f1 返回 int
}
decltype(auto) f2() {
int x = 0;
return (x); // decltype((x)) 是 int& ,f2 返回 int&
}
|
f2 返回了局部变量的引用,产生为定义行为。所以使用 decltype(auto) 时一定要小心,确保类型推导是想要的结果。