模板参数推导 函数模板类型推导 简单的函数模板:
Copy 1
2
template < typename T>
void f(ParamType param);
调用时:
在编译期间,编译器会推导两个类型:一个是 T ,另一个是 ParamType ,比如:
Copy 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 。 例子:
Copy 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 为右值,使用上面的推导规则。 例子:
Copy 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 ,那么再忽略之。 例子:
Copy 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 *
数组和函数实参 在很多上下文中,数组会退化为指向它一个元素的指针:
Copy 1
2
const char name[] = "Hello" ; // name 的类型是 const char[6]
const char * ptrToName = name; // 数组退化为指针
将数组类型传值给模板,会推导出一个指针类型,因为函数的数组声明会被视为指针声明,即 void f(int param[]);
等价于 void f(int* param);
。
Copy 1
2
3
4
template < typename T>
void f(T param);
f(name); // T 是 const char*
但是函数可以接受指向数组的引用,所以如果 ParamType 为引用,T 会被推导为真正的数组类型。
Copy 1
2
3
4
template < typename T>
void f(T& param);
f(name); // T 是 const char[13] ,ParamType 是 const char(&)[13]
函数类型和数组类似,也会退化为一个函数指针。
Copy 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]
时才能推导:
Copy 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 (& param)[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 的角色,下面是理想化的推导过程:
Copy 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 类型说明符的三种情况:
类型说明符是一个个指针或引用但不是万能引用 类型说明符是一个万能引用 类型说明符既不是指针也不是引用 Copy 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&&
数组和函数一样:
Copy 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
Copy 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 ,如果有多个参数,推导失败 Copy 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 函数的形参,使用的机制是模板类型推导,所以无法从直接初始化列表推导。
Copy 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 声明变量必须初始化,能够避免使用未初始化的变量Copy 1
2
3
int x; // 未初始化的变量
auto x2; // 错误,必须初始化
auto x3 = 0 ;
auto 能够表示只有编译器才知道的类型,比如声明 lambda 函数Copy 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 可以解决依赖机器的类型问题Copy 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 可以避免意识不到的类型不匹配错误Copy 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 有时推导出的类型并不是我们需要的类型,常见于使用代理类并具有隐式类型转换
Copy 1
2
3
4
5
std:: vector< bool > features(const Widget& w); // bool 表示 Widget 是否有对应的 feature
// 是否具有高优先级
bool highPriority = features(w)[5 ];
processWidget(w, highPriority);
如果使用 auto ,就会导致未定义行为
Copy 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 但是进行一次显示转换
Copy 1
auto highPriority = static_case< bool > (features(w)[5 ]);
使用 decltype decltype 返回名字或者表达式的类型。
Copy 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 主要用于函数模板返回类型,而这个返回类型依赖形参
Copy 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) ,上面可以优化一下
Copy 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) 时,加上括号就会导致类型不同。
Copy 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) 时一定要小心,确保类型推导是想要的结果。