TODO
int i = 0;
{{ icon.fun }} 人类的本质是复读机。
QSlider *slider = new QSlider();
std::shared_ptr<Test> test = std::make_shared<Test>();
auto slider = new QSlider();
auto test = std::make_shared<Test>();
TODO
在 C++98 时代,仅仅只是保存个迭代器作为变量,就得写一长串:
std::map<std::string, int> tab;
std::map<std::string, int>::iterator it = tab.find("key");
这踏码的类型名比右侧的表达式都长了!
{{ icon.fun }} 哮点解析:张心欣的第三条腿比另外两条腿都长。
有了 auto
以后,无需复读类型名和繁琐的 ::iterator
废话,自动从右侧 find
函数的返回值推导出正确的类型。
std::map<std::string, int> tab;
auto it = tab.find("key");
因为 auto
规定必须右侧有赋初始值(否则无法推导类型)。
所以只要你的代码规范能一直使用 auto
的话,就可以避免未初始化。
众所周知,读取一个未初始化的变量是未定义行为,C/C++ 程序员饱受其苦,小彭老师也好几次因为忘记初始化成员指针。
例如,你平时可能一不小心写:
int i;
cout << i; // 未定义行为!此时 i 还没有初始化
但是如果你用了 auto
,那么 auto i
就会直接报错,提醒你没有赋初始值:
auto i; // 编译出错,强制提醒你必须赋初始值!
cout << i;
你意识到自己漏写了 = 0
!于是你写上了初始值,编译才能通过。
auto i = 0;
cout << i;
可见,只要你养成“总是 auto
”的好习惯,就绝对不会忘记变量未初始化,因为 auto
会强制要求有初始值。
假设你有一个能返回 int
的函数:
int getNum();
有多处使用了这个函数:
int a = getNum();
...
int b = getNum() + 1;
...
假如你哪天遇到牢板需求改变,它说现在我们的 Num
需要是浮点数了!
float getNum();
哎呀,你需要把之前那些“多处使用”里写的 int
全部一个个改成 float
!
float a = getNum();
...
float b = getNum() + 1;
...
如果漏改一个的话,就会发生隐式转换,并且只是警告,不会报错,你根本注意不到,精度就丢失了!
现在“马后炮”一下,如果当时你的“多处使用”用的是 auto
,那该多好!自动适应!
auto a = getNum();
...
auto b = getNum() + 1;
...
无论你今天 getNum
想返回 float
还是 double
,只需要修改 getNum
的返回值一处,所有调用了 getNum
的地方都会自动适配!
{{ icon.fun }} 专治张心欣这种小计级扒皮牢板骚动反复跳脚的情况,无需你一个个去狼狈的改来改回,一处修改,处处生效。
std::vector<int> aVeryLongName(5);
auto aVeryLongName = std::vector<int>(5);
TODO
有同学反映,他想要创建一个 size_t
类型的整数,初始化为 3。
size_t i = 3; // 3 是 int 类型,这里初始化时发生了隐式转换,int 转为了 size_t
i = 0xffffffffff; // OK,在 size_t 范围内(64 位编译器)
如果直接改用 auto
的话,因为 3
这个字面量是 int
类型的,所以初始化出来的 auto i
也会被推导成 int i
!
虽然目前初始只用到了 3
,然而这位同学后面可能会用到 size_t
范围的更大整数存入,就存不下了。
auto i = 3; // 错误!auto 会推导为 int 了!
i = 0xffffffffff; // 超出 int 范围!
由于 C++ 是静态编译,变量类型一旦确定就无法更改,我们必须在定义时就指定号范围更大的 size_t
。
为了让 auto
推导出这位同学想要的 size_t
类型,我们可以在 3
这个字面量周围显式写出类型转换,将其转换为 size_t
。
{{ icon.tip }} 显式类型转换总比隐式的要好!
auto i = (size_t)3; // 正确
这里的类型转换用的是 C 语言的强制类型转换语法 (size_t)3
,更好的写法是用括号包裹的 C++ 构造函数风格的强制类型转换语法:
auto i = size_t(3); // 正确
看起来就和调用了 size_t
的“构造函数”一样。这也符合我们前面说的统一写法,类型统一和值写在一起,以括号结合,更可读。
{{ icon.detail }} 顺便一提,
0xffffffffff
会是long
(Linux) 或long long
(Windows) 类型字面量,因为它已经超出了int
范围,所以实际上auto i = 0xffffffffff
会推导为long i
。字面量类型的规则是,如果还在int
范围内(0x7fffffff 以内),那这个字面量就是int
;如果超过了 0x7fffffff 但不超过 0xffffffff,就会变成unsigned int
;如果超过了 0xffffffff 就会自动变成long
(Linux) 或long long
(Windows) ;超过 0x7fffffffffffffff 则变成unsigned long
(Linux) 或unsigned long long
(Windows) ——这时和手动加ULL
等后缀等价,无后缀时默认int
,如果超过了int
编译器会自动推测一个最合适的。
如果需要其他类型的变量,改用 short(3)
,uint8_t(3)
配合 auto
不就行了,根本没必要把类型前置。
TODO
TODO
C++11 引入的 auto
关键字可以用作函数的返回类型,但它只是一个“占位”,让我们得以后置返回类型,并没有多大作用,所以 C++11 这版的 auto
非常残废。
auto f() -> int;
// 等价于:
int f();
{{ icon.fun }} 闹了半天,还是要写返回类型,就只是挪到后面去好看一点……
{{ icon.detail }} 当初引入后置返回类型实际的用途是
auto f(int x) -> decltype(x * x) { return x * x; }
这种情况,但很容易被接下来 C++14 引入的真正auto
返回类型推导平替了。
终于,C++14 引入了函数返回类型推导,auto
才算真正意义上能用做函数返回类型!它会自动根据函数中的 return
表达式推导出函数的返回类型。
auto f(int x) {
return x * x; // 表达式 `x * x` 的类型为 int,所以 auto 类型推导为 int
}
// 等价于:
int f() {
return x * x;
}
如果函数中没有 return
语句,那么 auto
会被自动推导为 void
,非常方便。
auto f() {
std::println("hello");
}
// 等价于:
void f() {
std::println("hello");
}
值得注意的是,返回类型用 auto
来推导的函数,如果有多条 return
语句,那么他们必须都返回相同的类型,否则报错。
auto f(int x) {
if (x > 0) {
return 1; // int
} else {
return 3.14; // double
}
} // 错误:有歧义,无法确定 auto 应该推导为 int 还是 double
auto
还有一个缺点是,无法用于“分离声明和定义”的情况。因为推导 auto
类型需要知道函数体,才能看到里面的 return
表达式是什么类型。所以当 auto
返回类型被用于函数的非定义声明时,会直接报错。
auto f(); // 错误:看不到函数体,无法推导返回类型
auto f() { // 编译通过:auto 推导为 int
return 1; // 1 是 int 类型的表达式
}
因此,auto
通常只适用于头文件中“就地定义”的 inline
函数,不适合需要“分离 .cpp 文件”的函数。
返回类型声明为 auto
,可以自动推导返回类型,但总是推导出普通的值类型,绝对不会带有引用或 const
修饰。
如果需要返回一个引用,并且希望自动推导引用的类型,可以写 auto &
。
int i;
int &ref = i;
auto f() { // 返回类型推导为 int
return i;
}
auto f() { // 返回类型推导依然为 int
return ref;
}
auto &f() { // 返回类型这才能推导为 int &
return ref;
}
auto &f() { // 编译期报错:1 是纯右值,不可转为左值引用
return 1;
}
auto &f() { // 运行时出错:空悬引用是未定义行为
int local = 42;
return local;
}
这里的 auto
还可以带有 const
修饰,例如 auto const &
可以让返回类型变成带有 const
修饰的常引用。
int i;
int &ref = i;
```cpp
int i;
auto getValue() { // 返回类型推导为 int
return i;
}
auto &getRef() { // 返回类型推导为 int &
return i;
}
auto const &getConstRef() { // 返回类型推导为 int const &
return i;
}
{{ icon.tip }}
auto const &
与const auto &
完全等价,只是代码习惯问题。
有趣的是,如果 i
是 int const
类型,则 auto &
也可以自动推导为 int const &
且不报错。
const int i;
auto const &getConstRef() { // 返回类型推导为 int const &
return i;
}
auto &getRef() { // 返回类型也会被推导为 int const &
return i;
}
int &getRef() { // 报错!
return i;
}
{{ icon.tip }}
int const
与const int
是完全等价的,只是代码习惯问题。
{{ icon.detail }}
auto &
可以兼容int const &
,而int &
就不能兼容int const &
!很奇怪吧?这是因为auto
不一定必须是int
,也可以是const int
这一整个类型。你可以把auto
看作和模板函数参数一样,模板函数参数的T &
一样可以通过将T = const int
从而捕获const int &
。
如果要允许 auto
推导为右值引用,只需写 auto &&
。
std::string str;
auto &&getRVRef() { // std::string &&
return std::move(str);
}
auto &getRef() { // std::string &
return str;
}
auto const &getConstRef() { // std::string const &
return str;
}
正如 auto &
可以兼容 auto const &
一样,由于 C++ 的某些特色机制,auto &&
其实也可以兼容 auto &
!
所以 auto &&
实际上不止支持右值引用,也支持左值引用,因此被称为“万能引用”。
也就是说,其实我们可以都写作 auto &&
!让编译器自动根据我们 return
语句的表达式类型,判断返回类型是左还是右引用。
std::string str;
auto &&getRVRef() { // std::string &&
return std::move(str);
}
auto &&getRef() { // std::string &
return str;
}
auto const &getConstRef() { // std::string const &
return str;
}
auto &&
不仅能推导为右值引用,也能推导为左值引用,常左值引用。
可以理解为集合的包含关系:auto &&
> auto &
> auto const &
所以 auto &&
实际上可以推导所有引用,不论左右。
{{ icon.detail }} 这里的原因和刚才
auto = int const
从而auto &
可以接纳int const &
一样,auto &&
可以接纳int &
是因为 C++ 特色的“引用折叠”机制:& && = &
即左引用碰到右引用,会得到左引用。所以编译器可以通过令auto = int &
从而使得auto && = int & && = int &
,从而实际上auto &&
看似是右值引用,但是因为可以给auto
带入一个左值引用int &
,然后让左引用&
与右引用&&
“湮灭”,最终只剩下一个左引用&
,在之后的模板函数专题中会更详细介绍这一特色机制。
这就是为什么 int &&
就只是右值引用,而 auto &&
以及 T &&
则会叫做万能引用。一旦允许前面的参数为 auto
或者模板参数,就可以代换,就可以实现左右通吃。
返回类型声明为 decltype(auto)
的效果等价于把返回类型替换为 decltype((返回表达式))
:
int i;
decltype(auto) func() {
return i;
}
// 等价于:
decltype((i)) func() {
return i;
}
// 等价于:
int &func() {
return i;
}
{{ icon.warn }} 注意
decltype(i)
是int
而decltype((i))
是int &
。这是因为decltype
实际上有两个版本!当decltype
中的内容只是单独的一个标识符(变量名)时,会得到变量定义时的类型;而当decltype
中的内容不是单纯的变量名,而是一个复杂的表达式时,就会进入decltype
的第二个版本:表达式版,会求表达式的类型,例如当变量为int
时,表达式(i)
的类型是左值引用,int &
,而变量本身i
的类型则是int
。此处加上()
就是为了让decltype
被迫进入“表达式”的那个版本,decltype(auto)
遵循的也是“表达式”这个版本的结果。
int i;
decltype(auto) func() {
return i;
}
// 等价于:
decltype((i + 1)) func() {
return i + 1;
}
// 等价于:
int func() {
return i + 1;
}
int i;
decltype(auto) func() {
return std::move(i);
}
// 等价于:
decltype((std::move(i))) func() {
return std::move(i);
}
// 等价于:
int &&func() {
return std::move(i);
}
以上介绍的这些引用推导规则,其实也适用于局部变量的 auto
,例如:
auto i = 0; // int i = 0
auto &ref = i; // int &ref = i
auto const &cref = i; // int const &cref = i
auto &&rvref = std::move(i); // int &&rvref = move(i)
decltype(auto) j = i; // int j = i
decltype(auto) k = ref; // int &k = ref
decltype(auto) l = cref; // int const &l = cref
decltype(auto) m = std::move(rvref); // int &&m = rvref
众所周知,在 C++11 的“范围 for 循环” (range-based for loop) 语法中,auto
的出镜率很高。
但是如果只是写 auto i: arr
的话,这会从 arr 中拷贝一份新的 i
变量出来,不仅产生了额外的开销,还意味着你对这 i
变量的修改不会反映到 arr
中原本的元素中去。
std::vector<int> arr = {1, 2, 3};
for (auto i: arr) { // auto i 推导为 int i,会拷贝一份新的 int 变量
i += 1; // 错误的写法,这样只是修改了 int 变量
}
print(arr); // 依然是 {1, 2, 3}
更好的写法是 auto &i: arr
,保存一份对数组中元素的引用,不仅避免了拷贝的开销(如果不是 int
而是其他更大的类型的话,这是一笔不小的开销),而且允许你就地修改数组中元素的值。
std::vector<int> arr = {1, 2, 3};
for (auto &i: arr) { // auto &i 推导为 int &i,保存的是对 arr 中原元素的一份引用,不发生拷贝
i += 1; // 因为 i 现在是对 arr 中真正元素的引用,对其修改也会成功反映到原 arr 中去
}
print(arr); // 变成了 {2, 3, 4}
如果不打算修改数组,也可以用 auto const &
,让捕获到的引用添加上 const
修饰,避免一不小心修改了数组,同时提升代码可读性(人家一看就懂哪些 for 循环是想要修改原值,哪些不会修改原值)。
std::vector<int> arr = {1, 2, 3};
for (auto const &i: arr) { // auto const &i 推导为 int const &i,保存的是对 arr 中原元素的一份常引用,不发生拷贝,且不可修改
i += 1; // 编译期出错!const 引用不可修改
}
{{ icon.tip }} 对于遍历
std::map
,由于刚才提到的auto &
实际上也兼容常引用,而 map 的值类型是std::pair<const K, V>
,所以即使你只需修改V
的部分,只需使用auto &
配合 C++17 的“结构化绑定” (structural-binding) 语法拆包即可,K
的部分会自动带上const
,不会出现编译错误的。
std::map<std::string, std::string> table;
for (auto &[k, v]: table) { // 编译通过:k 的部分会自动带上 const
k = "hello"; // 编译出错:k 推导为 std::string const & 不可修改
v = "world"; // 没问题:v 推导为 std::string & 可以就地修改
}
C++20 引入了模板参数推导,可以让我们在函数参数中也使用 auto
。
在函数参数中也使用 auto
实际上等价于将该参数声明为模板参数,仅仅是一种更便捷的写法。
void func(auto x) {
std::cout << x;
}
// 等价于:
template <typename T>
void func(T x) {
std::cout << x;
}
func(1); // 自动推导为调用 func<int>(1)
func(3.14); // 自动推导为调用 func<double>(3.14)
如果参数类型的 auto
带有如 auto &
这样的修饰,则实际上等价于相应模板函数的 T &
。
// 自动推导为常引用
void func(auto const &x) {
std::cout << x;
}
// 等价于:
template <typename T>
void func(T const &x) {
std::cout << x;
}
// 自动推导为万能引用
void func(auto &&x) {
std::cout << x;
}
// 等价于:
template <typename T>
void func(T &&x) {
std::cout << x;
}
传统的,基于类型重载的:
int square(int x) {
return x * x;
}
double square(double x) {
return x * x;
}
int main() {
square(2); // 4(调用 int 版重载)
square(3.14); // 9.8596(调用 double 版重载)
// 如果现在又需要 float 版呢?又得写一版重载,内容还是完全一样的,浪费时间
}
基于 auto
模板参数推导的:
auto square(auto x) {
return x * x;
}
int main() {
square(2); // 4(auto 推导为 int)
square(3.14); // 9.8596(auto 推导为 double)
// 即使未来产生了 float 版的需求,也不用添加任何代码,因为是 square 是很方便的模板函数
}
实际上等价于模板函数的如下写法:
template <class T>
decltype(T() * T()) square(T x) {
return x * x;
}
和之前变量 auto
,返回类型 auto
的 auto &
、auto const &
、auto &&
大差不差,C++20 这个参数 auto
同样也支持推导为引用。
void passByValue(auto x) { // 参数类型推导为 int
x = 42;
}
void passByRef(auto &x) { // 参数类型推导为 int &
x = 42;
}
void passByConstRef(auto const &x) { // 参数类型推导为 int const &
x = 42; // 编译期错误:常引用无法写入!
}
int x = 1;
passByValue(x);
cout << x; // 还是 1
passByRef(x);
cout << x; // 42
void passByRef(auto &x) {
x = 1;
}
int x = 1;
const int const_x = 1;
passByRef(i); // 参数类型推导为 int &
passByRef(const_x); // 参数类型推导为 const int &
由于 auto &
兼容 auto const &
的尿性,此处第二个调用 passByRef
会把参数类型推导为 const int &
,这会导致里面的 x = 42 编译出错!
- 所以
auto &
实际上也允许传入const
变量的引用,非常恼人,不要掉以轻心。 - 而
auto const &
则可以安心,一定是带const
的。
{{ icon.fun }} 所以实际上最常用的是
auto const &
。
不仅如此 auto const &
参数还可以传入纯右值(利用了 C++ 可以自动把纯右值转为 const
左引用的特性)。
对于已有的变量传入,可以避免一次拷贝;对于就地创建的纯右值表达式,则自动转换,非常方便。
void passByConstRef(auto const &cref) {
std::cout << cref;
}
int i = 42;
passByConstRef(i); // 传入 i 的引用
passByConstRef(42); // 利用 C++ 自动把纯右值 “42” 自动转为 const 左值的特性
对于这种自动转出来的 const
左值引用,其实际上是在栈上自动创建了一个 const
变量保存你临时创建的参数,然后在当前行结束后自动析构。
passByConstRef(42);
// 等价于:
{
const int tmp = 42;
passByConstRef(tmp); // 传入的是这个自动生成 tmp 变量的 const 引用
}
这个自动生成的 tmp
变量的生命周期是“一条语句”,也就是当前分号结束前,该变量的生命周期都存在,直到分号结束后才会析构,所以如下代码是安全的:
void someCFunc(const char *name);
someCFunc(std::string("hello").c_str());
{{ icon.detail }} 此处
std::string("hello")
构造出的临时string
类型变量的生命周期直到;
才结束,而这时someCFunc
早已执行完毕返回了,只要someCFunc
对name
的访问集中在当前这次函数调用中,没有把name
参数存到全局变量中去,就不会有任何空悬指针问题。
TODO
然而,由于 C++ “默认自动变左值”的糟糕特色,即使你将一个传入时是右值的引用直接转发给另一个函数,这个参数也会默默退化成左值类型,需要再 std::move
一次才能保持他一直处于右值类型。