类型推导
C++11/14/17 学习的第二篇:模板类型推导、auto
、decltype
。
模板类型推导
函数模板通常写法如下:
template<typename T>
void f(ParamType param); // ParaType 为含有 T 的类型表达式
调用这个模板:
f(expr);
在编译的时候,编译器通过 expr
来进行推导出两个类型:一个是 T
的,另一个是ParamType
。这两个类型不一定相同。比如:
template<typename T>
void f(const T& param);
如果有这样的调用:
int x = 0;
f(x);
T
被推导成 int
,而 ParamType
被推导成 const int&
。
-
当
ParamType
为引用或者指针时- 如果
expr
的类型是个引用,忽略引用的部分 - 然后利用
expr
的类型和ParamType
对比去判断T
的类型。
下面是一个例子:
template<typename T> void f(T& param); int a = 1; // a 为 int const int b = a; // b 为 const int const int& c = a; // c 为 const int 的引用 f(a); // T 为 int,param 为 int& f(b); // T 为 const int,param 为 const int& f(c); // T 为 const int,param 为 const int&
左值引用和右值引用在此处没有区别。
若
T&
变成const T&
,则原来的const
会被扔掉。下面是一个例子:template<typename T> void f(const T& param); int a = 1; // a 为 int const int b = a; // b 为 const int const int& c = a; // c 为 const int 的引用 f(a); // T 为 int,param 为 const int& f(b); // T 为 int,param 为 const int& f(c); // T 为 int,param 为 const int&
若
param
是一个指针,推导方法基本相同。下面是一个例子:template<typename T> void f(T* param); int a = 1; // a 为 int const int *b = &a; // b 为指向 const int a 的指针 f(&a); // T 为 int,param 为 int* f(b); // T 为 const int,param 为 const int*
- 如果
-
当
ParamType
为万能引用时- 如果
expr
是一个左值,T
和ParamType
都会被推导成左值引用 - 如果
expr
是一个右值,那么就会按照前一种情况正常推导
下面是一个例子:
template<typename T> void f(T&& param); int a = 1; // a 为 int const int b = a; // b 为 const int const int& c = a; // c 为 const int 的引用 f(a); // a 为左值,故 T 为 int&,param 为 int& f(b); // b 为左值,故 T 为 const int&,param 为 const int& f(c); // c 为左值,故 T 为 const int&,param 为 const int& f(1); // 1 为右值,故 T 为 int,param 为 int&&
这里运用了引用折叠,具体可以参见我上一篇文章。
- 如果
-
当
ParamType
为普通类型时- 此时直接当作值传递。
- 忽略掉所有的引用、
const
、volatile
下面是一个例子:
template<typename T> void f(T param); int a = 1; // a 为 int const int b = a; // b 为 const int const int& c = a; // c 为 const int 的引用 f(a); // T 为 int,param 为 int f(b); // T 为 int,param 为 int f(c); // T 为 int,param 为 int
这相当于在传参时,对原来的变量做了一份拷贝。
-
传递数组时
数组可以退化为指针。
下面是一个例子:
template<typename T> void f(T param); const char a[] = "HelloWorld"; f(a); // T 为 const char*
然而,数组和指针并不完全相同。当
ParamType
为引用时,会推导出带有大小的数组。下面是一个例子:
template<typename T> void f(T& param); const char a[] = "HelloWorld"; f(a); // T 为 const char[10],param 为 const char(&)[10]
-
传递函数时
类似的,函数也会退化为指针。
下面是一个例子:
template<typename T> void f1(T param); template<typename T> void f2(T& param); void a(int, double); f1(a); // param 为 void(*)(int, double) f2(a); // param 为 void(&)(int, double)
根据以上内容,我们可以总结:
- 引用会被忽略
- 万能引用中,左值会被特殊处理
- 按值传递时,cv 特性会被忽略
- 数组和函数会被退化为指针,除非是用在引用类型
auto
类型推导
auto
类型推导就是模板类型推导。
下面是几个例子:
const char g[] = "HelloWorld";
void j(int, double);
auto a = 1; // a 为 int
const auto b = a; // b 为 const int
const auto& c = a; // c 为 const int&
auto&& d = a; // a 为左值,故 d 为 int&
auto&& e = b; // b 为左值,故 e 为 const int&
auto&& f = 1; // 1 为右值,故 f 为 int&&
auto h = g; // h 为 const char*
auto& i = g; // i 为 const char(&)[10]
auto k = j; // k 为 void (*)(int, double)
auto& k = j; // k 为 void (&)(int, double)
但是,在统一初始化时,auto
推导会有所不同。
下面是一个例子:
auto a = 1; // a 为 int,值为 1
auto b(1); // b 为 int,值为 1
auto c = { 1 }; // c 为 std::intializer_list<int>,值为 { 1 }
auto d{ 1 }; // d 为 std::intializer_list<int>,值为 { 1 }
auto e = { 1, 2, 3 }; // e 为 std::intializer_list<int>
auto f = { 1, 2, 3.0 }; // 无法推导,编译错误
而在模板版类型推导中,却有
template<typename T>
void f1(T param);
template<typename T>
void f2(std::initializer_list<T> initList);
f1({ 1, 2, 3 }); // 无法推导,编译错误
f2({ 1, 2, 3 }); // T 为 int,initList 为 std::initializer_list<int>
在 C++14 中,允许 auto
表示推导的函数返回值,且lambda 可能会在参数声明里面使用 auto
。但是此处的推导却直接使用的和模板一样的推导,而不是 auto
类型推导。
下面是一个例子:
auto a() {
return { 1, 2, 3 }; // 无法推导,编译错误
}
auto b = [&c](const auto& d) { c = d; }
b({ 1, 2, 3 }); // 无法推导,编译错误
根据以上内容,我们可以总结:
-
auto
类型推导和模板类型推导几乎相同 -
auto
类型推导统一初始化为std::initializer_list
-
auto
在函数返回值或者 lambda 参数里面执行模板类型推导
decltype
使用
decltype
用来判断变量或者表达式类型。它一般只是复述一遍你所给他的变量名或者表达式的类型。
下面是几个例子:
const int a = 0; // decltype(a) 为 const int
bool b(const int& c); // decltype(c) 为 const int&,decltype(b) 为 bool(const int&)
struct d{ int e, f; }; // decltype(d::e) 为 int
vector<int> g; // decltype(g) 为 vector<int>,decltype(g[0]) 为 int&
decltype
最主要的用处是声明一个函数模板,使得这个函数模板的返回值类型取决于参数的类型。
下面是一个例子:
template<typename Container, typename Index>
auto f(Container& c, Index i)
-> decltype(c[i]) {
return c[i];
}
到了 C++14,我们这样写也是对的:
template<typename Container, typename Index>
auto f(Container& c, Index i) {
return c[i];
} // 返回了 int
然而,这还是有问题。假设 c
中对象的类型为 int
,则 c[i]
返回的类型为 int&
,经过 auto
后,引用会被忽略,变为 int
。这时,返回的就是右值而不是左值。如果想要返回左值,则必须这样写:
template<typename Container, typename Index>
decltype(auto) f(Container& c, Index i) {
return c[i];
} // 返回了 int&
decltype(auto)
也可以使用在变量声明上。
下面是一个例子:
int a;
const int& b = a; // b 为 const int&
auto c = b; // c 为 int
decltype(auto) d = b; // d 为 const int&
上面讲的函数参数均为左值。如果希望有右值引用参数,则需要这样写:
template<typename Container, typename Index>
auto authAndAccess(Container&& c, Index i)
-> decltype(std::forward<Container>(c)[i]) {
return std::forward<Container>(c)[i];
}
在 C++14 中改为:
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i) {
return std::forward<Container>(c)[i];
}
以上情况中,decltype
都是什么就输出什么。然而在某些情况下并非如此。
我们知道,如果给 decltype
一个类型为 T
的左值表达式,其会给出 T&
。
下面是一个例子:
decltype(auto) f1() {
int x = 0;
return x; // decltype(x) 为 int
}
decltype(auto) f2() {
int x = 0;
return (x); // decltype((x)) 为 int&
}
可以看到,仅仅一个左值与左值表达式的情况大相径庭。
根据以上内容,我们可以总结:
- C++14 支持
decltype(auto)
,其推导auto
类型时使用decltype
的规则 - 对于非变量名的、类型为
T
的左值表达式,decltype
返回T&
查看类型推导结果
-
IDE 查看
现在大部分 ide 都支持查看类型推导结果。例如,在 vscode 中,只需要在写代码时把鼠标移动到变量上,就能显示变量的类型。
然而,如果类型较为复杂,ide 的显示结果很可能是错误的。
-
编译器诊断
比如我们定义了如下变量:
const int a = 1; auto b = a; auto c = &a;
我们可以利用编译器来查看变量类型:
template<typename T> class TypeDisplay; TypeDisplay<decltype(b)> bType; TypeDisplay<decltype(c)> cType;
编译时,编译器会报错:
error: aggregate 'TypeDisplay<int> bType' has incomplete type and cannot be defined error: aggregate 'TypeDisplay<const int*> cType' has incomplete type and cannot be defined
这样就看到了
decltype
推导出的类型。 -
运行时输出
我们可以使用
typeid
来输出类型。下面是一个例子:
const int a = 1; auto b = a; auto c = &a; std::cout << typeid(b).name() << '\n'; std::cout << typeid(c).name() << '\n';
运行后输出
i PKi
其中,
i
表示 int,P 为 pointer,k 为 c(k)onst。
参考资料
- Effective Modern C++, Scott Meyers
Comments