推导类型
1. 理解类型推导
auto的推导方式和template是一样的,所以我们首先来介绍template是如何推导类型的。
template <typename T>
void f(const T& orig) {
cout << __PRETTY_FUNCTION__ << endl;
cout << typeid (orig).name() << endl;
cout << typeid (T).name() << endl;
}
int x = 10;
f(x);
/*
void f(const T &) [T = int]
i
i
*/
T和orig的类型一样的,这很奇怪吧。实际上,template类型推导有三个情况:
-
- orig是一个指针或者引用类型,但不是全局引用(universal reference)
-
- orig是一个全局引用。
-
- orig即使不是指针也不是引用。
template <typename T>
void f(ParamType param);
f(expr)
情况1 :ParamType是一个指针或者引用类型,但不是全局引用(universal reference)
在这种情况下,
-
- 如果expr的类型是一个引用,忽略引用的部分。
-
- 把expr的类型与ParamType的类型比较,用来判断T的类型。
例如:
template <typename T>
void f(T& param) {
cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
int y = 10;
f(y);
const int x = y;
f(x);
const int& z = y;
f(z); // ignore the reference.
return 0;
}
void f(T &) [T = int]
void f(T &) [T = const int]
void f(T &) [T = const int]
这就是为什么一个const对象传给模板后是安全的,因为const性质会成为模板推导的一部分。
我们注意到第三个例子中,T被推导为const int,是因为忽略了&, 如果不这样,ParamType 会被推导为const int&&,这是不被允许的。
我们这里提及的都是左值引用,实际上右值引用也是一样的,但是右值引用只能传递给右值引用,虽然这个类型推导关系不大。
我们来做一个小小的修改。
template <typename T>
void f(const T& param) {
cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
int y = 10;
f(y);
const int x = y;
f(x);
const int& z = y;
f(z); // ignore the reference.
return 0;
}
void f(const T &) [T = int]
void f(const T &) [T = int]
void f(const T &) [T = int]
同样的,T的引用被忽略了,const属性也被忽略了。因为对于param而言总是const。
对于指针:
template <typename T>
void f(T* param) {
cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
int y = 10;
f(&y);
const int x = y;
f(&x);
const int& z = y;
f(&z); // ignore the reference.
const int* p = &z;
f(p);
return 0;
}
void f(T *) [T = int]
void f(T *) [T = const int]
void f(T *) [T = const int]
void f(T *) [T = const int]
分析也一样。
情况2:ParamType是一个全局引用。
全局引用是T&&类型,也就是右值引用。这种情况稍微有一些不同。
-
- 如果expr是一个左值引用,那么T和ParamType都会被推导为左值引用。
-
- 如果expr是一个右值,那么就使用通常的推导方法。
例如:
template <typename T>
void f(T&& param) {
cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
int y = 10;
f(y);
const int x = y;
f(x);
const int& z = y;
f(z);
f(10);
return 0;
}
void f(T &&) [T = int &] // param为int &
void f(T &&) [T = const int &]
void f(T &&) [T = const int &]
void f(T &&) [T = int] // param 为int&&
关键是,当使用全局引用时,类型推导会区别于右值引用和左值引用。
情况3: ParamType既不是指针也不是引用时
-
- 如果expr是一个引用,忽略引用。
-
- 如果expr是一个const, 忽略const。如果expr是一个volatile,忽略它。
例如:
template <typename T>
void f(T param) {
cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
int y = 10;
f(y);
const int x = y;
f(x);
const int& z = y;
f(z);
return 0;
}
void f(T) [T = int]
void f(T) [T = int]
void f(T) [T = int]
因为这里是按值传递,所以本身对象的性质并不会传递给他的拷贝对象。
前面提到在类型推导时,const引用或者constpointer的const属性会被保留,但如果expr是一个const指针指向const对象,而expr被传递给按值传递的函数,那么情况会怎么样?
template <typename T>
void f(T param) {
cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
const char* const ptr = "yanzexin";
f(ptr);
return 0;
}
void f(T) [T = const char *]
const char* const ptr = "yanzexin";
第一个const,指不可以修改这个指针指向的对象。
第二个const,指不可以修改指针指向的对象的值。
在传递过程中,指针的const属性被保留,但指针指向对象的const属性被忽略了。
对于数组
数组通常情况下都会被理解为指向数组第一个元素的指针。
template <typename T>
void f(T param) {
cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
const char name[] = "stary";
f(name);
const int phone[] = {1, 2};
f(phone);
return 0;
}
void f(T) [T = const char *]
void f(T) [T = const int *]
但如果我们真的希望传递的是一个数组,我们可以使用引用。
template <typename T>
void f(T& param) {
cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
const char name[] = "stary";
f(name);
const int phone[] = {1, 2};
f(phone);
return 0;
}
void f(T &) [T = char const[6]]
void f(T &) [T = int const[2]]
T会真正的被推导为数组。由此param的类型实际上就是const char&[6]。 所以我们甚至可以直接推导出数组的大小。
template <typename T, size_t N>
void f(T(&) [N]) {
cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
const char name[] = "stary";
f(name);
const int phone[] = {1, 2};
f(phone);
return 0;
}
void f(T (&)[N]) [T = const char, N = 6]
void f(T (&)[N]) [T = const int, N = 2]
对于函数
函数实际上也是和数组一样,会被自动推导到指针。如果希望推导成引用,方法是一样的。
template <typename T>
void f(T param) {
cout << __PRETTY_FUNCTION__ << endl;
}
void func(double, int) {
}
int main(int argc, char *argv[]) {
f(func);
return 0;
}
void f(T) [T = void (*)(double, int)]
template <typename T>
void f(T& param) {
cout << __PRETTY_FUNCTION__ << endl;
}
void func(double, int) {
}
int main(int argc, char *argv[]) {
f(func);
return 0;
}
void f(T &) [T = void (double, int)]
关键点:
-
- 当推导类型为指针或非全局引用,引用性会被忽略。
-
- 当推导类型为全局引用时,左值引用被推导为左值引用,右值引用被推导为右值引用。
-
- 当为按值传递时,推导的引用和const属性被忽略。
-
- 数组和函数会被推导为指针。
2. 理解auto的类型推导
auto就是使用template的推导方式,实际上存在一种直接的映射,把template的推导和auto的类型推导联系起来。
int main(int argc, char *argv[]) {
auto x = 17; // x = int
auto& y = x; // y = int&
const auto& i = x; // i = const int&
auto z = y; // z = int
const auto f = y; // f = const int
auto& m = i; // m = const int&
auto&& t = i; // t = int&
auto&& t1 = 10; // t1 = int&&
return 0;
}
int main(int argc, char *argv[]) {
int A[3] = {1, 2, 3};
auto a = A; // a = int *
auto& a1 = A; // a1 = int (&)[3]
return 0;
}
可以发现和auto确实没什么区别。
实际上,auto和template只有一个很大的区别。
在C++11中给出了一个全新的初始化方法,叫参数列表。
int main(int argc, char *argv[]) {
int a0 = (1); // a0 = int
auto a1 = (1); // a1 = int
int a2 = {1}; // a0 = int
auto a3 = {1}; // a3 = std::initializer_list<int>
return 0;
}
但需要注意的是,参数列表在auto中会被推导为 std::initializer_list。
auto中可以把带{}的推导出来,但template是推导不出来的,编译无法通过。
template <typename T>
void test(T orig) {
cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
test((1));
test({1}); // error!
return 0;
}
在C++11中,这就已经没什么问题了。但C++14中还有一小部分的问题需要讨论。
C++14允许auto去指示函数的返回类型,并且允许在lambda表达式中使用auto参数(C++11中不允许),但这个auto的推导是使用template推导方式的,不是使用auto本身的推导方式。这也就是说,返回一个{},是不能通过编译的。
int main(int argc, char *argv[]) {
list<int> ls;
auto l = [&ls](auto&& list) {
ls.insert(ls.end(), list);
};
l(1);
return 0;
}
int main(int argc, char *argv[]) {
list<int> ls;
auto l = [&ls](auto&& list) {
ls = list;
};
// l({1, 2, 3, 4}); error!
l(list<int> {1, 2, 3, 4});
return 0;
}
关键点:
auto推导通常情况下和template的推导是一样的,除非一个变量被声明并且是使用的初始化列表。
3. 理解decltype
template <typename container, typename index>
auto re(container& con, index i) {
return con[i];
}
int main(int argc, char *argv[]) {
vector<int> a {1, 2, 3, 4, 5, 6};
re(a, 3) = 10; // 无法通过编译
cout << a[3] << endl;
return 0;
}
返回类型是auto,就有前面提到的一样vector []operator返回的是引用类型,但auto会自动把引用类型忽略,从而无法进行修改。但如果我们希望这个函数返回的是真正的引用类型,该怎么做呢?
使用decltype,显式表明返回类型。以下这个实现方法能实现但不够好。原因我们暂时不去解释。
template <typename container, typename index>
auto re(container& con, index i) ->decltype(con[i]) {
return con[i];
}
int main(int argc, char *argv[]) {
vector<int> a {1, 2, 3, 4, 5, 6};
re(a, 3) = 10;
cout << a[3] << endl;
return 0;
}
一种更简单的做法是:
template <typename container, typename index>
decltype(auto) re(container&& con, index i) {
return con[i];
}
int main(int argc, char *argv[]) {
vector<int> a{1, 2, 3, 4, 5};
re(a, 3) = 10;
cout << re(a, 3) << endl;
return 0;
}
auto表明这个类型需要推导,decltype表示推导使用decltype的方法,也就是根据他实际的类型来返回。这种方法更加好。但需要c++14。
decltype(auto)
不仅可以用作函数的返回类型,也可以用作变量的声明。
int main(int argc, char *argv[]) {
const int a = 10;
decltype(auto) b = a;
return 0;
}
b也是const int!
template <typename container, typename index>
decltype(auto) re(container& con, index i) {
return con[i];
}
这个容器只能传递非const左值引用。右值引用不能捆绑左值引用。(除非是const的左值引用)
我们之前提到的
template <typename container, typename index>
decltype(auto) re(container&& con, index i) {
return con[i];
}
做法不是太好。
具体的做法应该是
template <typename container, typename index>
decltype(auto) re(container& con, index i) {
return forward<container>(con)[i];
}
实际上是这样的C++给出标准,有对于右值引用既可以是左值也可以是右值,有名字的就是左值,没名字的就是右值。
forward只能在模板函数中私用, 它原本是什么类型就返回什么类型。于是我们可以做出这样的实例,来解释。
#include <iostream>
#include <vector>
using namespace std;
class test {
public:
test() {
cout << "construct" << endl;
}
test(test&& orig) {
cout << "move" << endl;
}
test(test& orig) {
cout << "copy" << endl;
}
};
template <typename T>
decltype(auto) f(T&& orig) {
return forward<T>(orig);
}
int main(int argc, char *argv[]) {
test b;
test c = f(b);
cout << endl;
test m = f(test());
return 0;
}
construct
copy
construct
move
Program ended with exit code: 0
结果就非常明显了。
具体关于move和forward的使用细节见:
最后一个问题是,decltype((x))为被推导为int&!这里需要注意的是,不要返回临时对象的引用。否则可能会出问题。
decltype(auto) f() {
int x = 0;
return (x);
}
这会导致不确定性行为。