本篇目标:
1.学会使用auto和decltype对类型的推导以及常见易错点
2.学会使用using,以及与typedef的区别
一.auto类型说明符
1.为什么要有auto?
我们应该比较清楚,在使用STL 容器时,类型往往会变得极其复杂,直接去拼写这些类型可能就容易出错,例如:
cpp
map<string, vector<int>> m;
for (map<string, vector<int>>::const_iterator it = m.begin(); it != m.end(); ++it)
{
// ...
}
遍历这个 map 需要写出极其冗长的迭代器类型,此时我们使用auto就可以很好的解决这个问题
cpp
for (auto it = m.begin(); it != m.end(); ++it)
{
// ...
}
此时冗长的代码就变成了较短的代码,但不可避免的降低了代码的可读性 ,所以有时我们需要在重要的部分添加一下注释,翻遍阅读时的理解
2.auto的使用规则
<1>.必须初始化,编译器必须通过等号右边的表达式来反推类型
例如:
cpp
auto a = 10; // 推导为 int
auto b = 3.14; // 推导为 double
// auto c; // 错误,编译器不知道c的类型?
<2>.同一行的类型必须绝对一致,一行代码里用auto声明多个变量,编译器只会对第一个变量进行推导,然后强行用这个类型去匹配后面的变量
例如:
cpp
auto x = 1, y = 2; // 都是 int
// auto a = 1, b = 3.14; // 错误!a 推导为 int,b 的推导结果 double 与 a 冲突
<3>.指针类型会保留
例如:
cpp
int x = 10;
int* p = &x;
auto a = p; // a的类型为int*
<4>.auto不能自动推导出引用类型,所以我们如果想将auto推导为引⽤类型,需要明确的指出: auto& x = i;
例如:
cpp
int a=10;
int&ra=a;
auto b=ra; // b的类型为int
auto&c=ra; //c的类型为int&
解释: 这是因为使用引用其实是使用使用引用的对象,特别是当引用被当作初始值时,真正参与初始化的其实是引用对象的值,所以当我们给b初始化时,真正参与初始化的其实是a
<5>.auto不能推导出顶层const,如果想使用auto推导出顶层const,需要明确的指出: const auto x = ci;但是却不会忽略底层const
例如:
cpp
int i = 0;
int& ri = i;
const int ci = 42; // 顶层const
int* const p1 = &i; // 顶层const
const int* p2 = &ci; // 底层const
const int& ri1 = ci; // 底层const
const int& ri2 = i; // 底层const
auto r1 = ci; // r1类型为int,忽略掉顶层const
r1++;
auto r2 = p1; // r2类型为int*,忽略掉顶层const
r2++;
auto r3 = p2; // r3类型为const int*,保留底层const
//(*r3)++ // 报错
<6>.设置⼀个类型为auto引用时,初始值中的顶层const属性仍然保留,否则存在权限放大问题。
cpp
const int a=10; // 顶层const
auto&ra=a; //ra的类型为const int&
//ra++; //报错
3.有关auto引用的补充
规则:
auto& 声明⼀个左值引用,它只能绑定到左值,如果初始化对象有const属性,推导时会保持 const 限定符,否则涉及权限放⼤。
• const auto& 声明⼀个const左值引用,既可以绑定到左值⼜可以绑定到右值,不会修改绑定 对象。
• auto&& 是万能引用,遵循引用折叠的规则,既可以绑定到左值又可以绑定到右值,初始化表达 式自动推导为左值引用或右值引用,如果初始化对象有const属性,推导时会保持const限定符。
例如:
cpp
#include<iostream>
#include<string>
#include<vector>
using namespace std;
void func(int& x)
{
cout << "void func(int& x)" << endl;
}
void func(int&& x)
{
cout << "void func(int&& x)" << endl;
}
void func(const int& x)
{
cout << "void func(const int& x)" << endl;
}
void func(const int&& x)
{
cout << "void func(const int&& x)" << endl;
}
int main()
{
int x = 10;
const int cx = 20;
auto& rx1 = x; // int&
auto& rx2 = cx; // const int&
func(rx1);
func(rx2);
const auto& rx3 = x; // const int&
const auto& rx4 = cx; // const int&
func(rx3);
func(rx4);
// 万能引⽤
auto&& rx5 = x; // int&
auto&& rx6 = cx; // const int&
func(rx5);
func(rx6);
auto&& rx7 = move(x); // int&&
//rx7++;
auto&& rx8 = move(cx); // const int&&
//rx8++;
return 0;
}
从以上的内容我们可以知道,编译器推导auto类型时,有时候也会和初始值的类型不⼀样,编译器会适当的改变结果类型,使其更符合初始化规则
4.auto不可以使用的地方
<1>.类的非静态成员变量
编译器在编译类的时候必须知道类的确切大小,如果允许 auto,编译器在没有实例化对象前,无法确定该成员变量占多少内存。
例如:
cpp
class MyClass
{
auto val = 10; // 错误!即使给了初始值也不行
auto name; // 错误!没有初始值更不行
};
注:只有静态常量成员可以使用。
<2>.作为模板的实例化参数
使用容器或者模板类的时候,必须明确告诉编译器底层装的是什么类型,auto 不能用来代替具体的模板类型。
例如:
cpp
std::vector<auto> v; // 错误!编译器不知道vector到底要装什么?
std::vector<int> v2; // 正确!
<3>.普通函数的参数(C++20 之前)
在 C++20 之前,普通函数的参数是绝对不能用 auto 的,因为普通函数不是模板,编译器在编译这个函数时,必须明确参数的类型。
例如:
cpp
C++20前的环境下:
void print(auto x)
{
// 错误!
cout <<x<<endl;
}
<4>.普通数组的直接声明
你不能用 auto 配合方括号 [] 来声明数组。
例如:
cpp
auto arr[3] = {1, 2, 3}; // 错误!
二.decltype类型指示符
1.decltype的使用规则
作用:返回操作数的数据类型
规则:
<1>.编译器分析表达式并得到它的类型,但是并不计算表达式的值
例如:
cpp
int f()
{
int a=10;
return a;
}
int main()
{
decltype(f()) sum=10; //返回类型为int
return 0;
}
解释:编译器并不调用函数f,而是使用f的返回值类型作为sum的类型
cpp
int f1();
const int& f2();
decltype(f1()) var4 = 1; // 类型是 int
decltype(f2()) var5 = i; // 类型是 const int&
解释:编译器并不调用函数f,而是使用f的返回值类型作为sum的类型
<2>.如果表达式是一个没有被多余括号包裹的普通变量名或类成员访问表达式,decltype 会直接返回该实体在代码中声明时的确切类型。
例如:
cpp
const int i = 0;
int& ri = i;
struct A { double x; };
const A* a = new A();
decltype(i) var1; // 类型是 const int
decltype(ri) var2 = i; // 类型是 int&
decltype(a->x) var3; // 类型是 double
解释:无论是底层const,还是引用与顶层const,都会返回对应的类型
<3>.其他表达式
如果不属于上述两种情况(例如它是一个算术表达式、带括号的变量名等),decltype 会根据该表达式的值类别 (Value Category) 和基础类型 T 来推导结果:
-
如果是左值 (lvalue): 结果为
T&(左值引用)。- 解释:能够取地址的表达式通常是左值。
-
如果是将亡值 (xvalue): 结果为
T&&(右值引用)。- 解释:例如
std::move()的返回值。
- 解释:例如
-
如果是纯右值 (prvalue): 结果为
T(非引用类型)。- 解释:例如普通的字面量或算术运算结果。
例如:
cpp
int a = 0, b = 0;
// 1. 纯右值 (prvalue)
decltype(a + b) var1; // a + b 返回一个临时值,是纯右值。类型为 int
decltype(100) var2; // 100 是字面量,纯右值。类型为 int
// 2. 左值 (lvalue)
decltype(a += b) var3 = a; // a += b 返回 a 的引用,是左值。类型为 int&
int arr[5];
decltype(arr[0]) var4 = a; // 数组访问返回左值。类型为 int&
// 3. 将亡值 (xvalue)
decltype(std::move(a)) var5 = 1; // std::move 返回右值引用,是将亡值。类型为 int&&
//
decltype((a)) var6=10 // 给a多加了一层(),被推导成了int&
auto必须要通过初始化值推导类型,像类的成员变量这种就没办法使用auto,decltype可以很好的 解决这样的问题,具体看代码样例:
cpp
#include <vector>
using namespace std;
template <typename T>
class A
{
public:
void func(T& container)
{
_it = container.begin();
}
private:
// 这⾥不确定是iterator还是const_iterator,也不能使⽤auto
typename T::iterator _it;
// 使⽤decltype推导就可以很好的解决问题
//decltype(T().begin()) _it;
};
int main()
{
const vector<int> v1;
A<const vector<int>> obj1;
obj1.func(v1);
vector<int> v2;
A<vector<int>> obj2;
obj2.func(v2);
return 0;
}
此时将typename T::iterator _it;改为decltype(T().begin()) _it;即可
2.尾置返回类型
****作用:允许你在声明或定义函数时,将函数的返回类型写在参数列表的后面,而不是像传统语法那样写在最前面。
基本语法:
cpp
auto functionName(parameters) -> returnType
{
// 函数体
...
}
例如传统写法:
cpp
int add(int a, int b);
尾置返回类型写法:
cpp
auto add(int a, int b) -> int;
为什么需要尾置返回类型?
<1>. 提高代码可读性:特别是当返回类型很长或复杂时
<2>. 支持Lambda表达式:Lambda表达式的返回类型必须使用尾置语法
<3>. 模板编程:在模板函数中,返回类型可能依赖于参数类型
例如:
cpp
#include <vector>
#include <list>
using namespace std;
template<class R, class Iter>
R Func(Iter it1, Iter it2)
{
R x = *it1;
++it1;
while (it1 != it2)
{
x += *it1;
++it1;
}
return x;
}
int main()
{
vector<int> v = { 1,2,3 };
list<string> lt = {"111","222","333"};
auto ret1 = Func(v.begin(), v.end());
auto ret2 = Func(lt.begin(), lt.end());
return 0;
}
这里无法调用上面的函数,因为函数模板只能通过实参推导模板类型,但是无法推导R
这样写也是不行的:
cpp
template<class Iter>
decltype(*it1) Func(Iter it1, Iter it2)
{
auto& x = *it1;
++it1;
while (it1 != it2)
{
x += *it1;
++it1;
}
return x;
}
因为 C++ 是前置语法,编译器遇到对象只会向前搜索
那么此时就需要尾置返回类型的帮助了,如代码所示:
cpp
template<class Iter>
auto Func(Iter it1, Iter it2)->decltype(*it1)
{
auto& x = *it1;
++it1;
while (it1 != it2)
{
x += *it1;
++it1;
}
return x;
}
3.decltype(auto)的使用规则
作用:让编译器自动推导类型,但推导时严格遵守 decltype 的规则
例如:
cpp
int main()
{
int i = 0;
int& ri = i;
const int ci = 42; // 顶层const
int* const p1 = &i; // 顶层const
const int* p2 = &ci; // 底层const
auto j = ri; // j类型为int
decltype(auto) j1 = ri; // j1类型为int&
++j1;
auto r1 = ci; // r1类型为int,忽略掉顶层const
decltype(auto) rr1 = ci; // rr1类型为const int
r1++;
//rr1++;
auto r2 = p1; // r2类型为int*,忽略掉顶层const
decltype(auto) rr2 = p1; // rr1类型为int* const
(*rr2)++;
//rr2 = p2; //报错
auto r3 = p2; // r3类型为const int*,保留底层const
decltype(auto) rr3 = p2; // rr3类型为const int*
// (*rr3)++;
return 0;
}
三.typedef与using的认识与使用
typedef 的逻辑是:typedef 原类型 新名字;
例如:
cpp
// 给 unsigned int 起个别名叫 uint
typedef unsigned int uint;
using 的逻辑是:using 新名字 = 原类型;
例如:
cpp
// using 的写法
using uint = unsigned int;
可以看出,两者都是给变量起个新的名字,但是是typedef不支持带模板参数的类型重 定义。C++11中新增了using可以替代typedef,using的别名语法覆盖了typedef的全部功能,是支持带模板参数重定义的语法。
例如:
cpp
using vec_int = vector<int>;
using map_int_str = unordered_map<int,string>;
using STDateType = int;
using Callback = void (*)(int);
template<class Val>
using map_str = unordered_map<Val, string>;
template<class Val>
using MapIter = typename map<string, Val>::iterator;
int main()
{
LL a = 123654789;
map_int_str map1;
map_str<int> map2 = { {1,"fasf"}};
return 0;
}