自动类型推导auto (C++11起)
介绍
auto 是现代 C++ 历史的开端之作,现代 C++ 一定是离不开 auto 的,这是因为一些历史包袱原因,C++ 的类型系统异常复杂,而引入了 auto 之后,代码则可以非常简洁。
C++98 时期,C++ 必须这样写:
using i64 = long long;
std::vector<int> a(100, 0);
for(std::vector<int>::iterator it = a.begin(); it != a.end(); ++it) {
// 操作it
}而且当你把变量 a 的类型改为 std::vector<i64> 时,你需要把你项目全部的有关 a.begin() 初始化的迭代器类型全部手动改为 std::vector<i64>::iterator,显然,这太唐了。
而 C++11,则有两种方式:
std::vector<int> a(100, 0);
for(auto it = a.begin(); it != a.end(); ++it) {
auto v = *it;
// 操作it
}
for(auto v : a) {
// 操作v
}INFO
对于 范围for 还没有介绍,实际上对于方法二的 范围for (C++11) 而言,它与方法一的写法是完全等价的,换言之,拥有 .begin() 与 .end() 的类(且其迭代器支持 ++ 和 * 两种运算)都可以作为 范围for 的对象,本质则是方法一。主要是需要知道,当你修改 a 的类型之后,你代码里其他的代码,全部无需改动,这是 auto 的意义。
变量声明
import std;
int main()
{
auto x = 0; // int
auto y = 0.; // double
auto z = 0LL; // i64
auto a = std::vector<int>(100, 0); // std::vector<int>
auto s = std::string{}; // std::string
// auto s2 = std::string; // 写法错误,因为这只写了个类型,而没有声明匿名对象
}当你编写使用 auto 的代码时,只是编译器会帮你把代码改成以下代码,仅此而已:
import std;
int main()
{
int x = 0; // int
double y = 0.; // double
i64 z = 0LL; // i64
std::vector<int> a = std::vector<int>(100, 0); // std::vector<int>
std::string s = std::string{}; // std::string
// auto s2 = std::string; // 写法错误,因为这只写了个类型,而没有声明匿名对象
}对于如下代码:
auto a = std::vector<int>(100, 0);
// 等价 std::vector<int> a = std::vector<int>(100, 0);INFO
其实质只是一个拷贝初始化而已,利用到了匿名对象。
返回类型声明 (C++11,C++14)
C++11引入了一种全新的表达函数返回类型的写法,尾置返回类型。
C++11,对下列尾置返回类型而言,auto 仅仅只是一个占位符的作用,千万不要觉得auto是自动推导返回类型,在C++11,auto就是一个占位符,告诉编译器我的写法是尾置返回类型。
auto func() -> void
{
return;
}
auto f2() -> int
{
return 0;
}WARNING
后面的 type 是必须写出的。
C++14,则进一步改进,允许返回类型是 auto 的,只要我们只写一个auto,就表示返回类型由编译器根据return自动推导。
auto func() // 推导为 void
{
return;
}
auto f2() // 推导为 int
{
return 0;
}那么尾置返回类型解决的问题是什么?有的时候我们是希望写出返回类型,增强我们代码的可读性。来看一个例子
auto func(auto x)
{
return x; // 返回 x
}这个函数的函数返回类型显然是x的类型,那么他的返回类型怎么写?当然存在一个这样的写法。
auto func(auto x) -> auto
{
return x;
}但是似乎与我们想表达的不太一致,他仅仅表达的是我们显式指定返回类型是auto的,而不是返回x的类型,那么此时我们可以这么写。
auto func(auto x) -> decltype(x)
{
return x;
}decltype是C++11引入的关键字,其与sizeof都是编译期确定值,只不过decltype表达的是括号内的类型,比如decltype(x)表示的就是x的类型,当然有关decltype的各种细节很多,这里暂不赘述,只需要知道他是什么意思就好了。但是你可能有个疑惑,为什么不这样写?
decltype(x) func(auto x) // 错误!
{
return x;
}不能这么写单纯是因为,当编译器编译的时候,他只能看到他前面的东西,当编译器编译decltype(x)的时候,他不知道x是什么,就会去前面找,进而导致错误。
来看另一个解决的问题,不知道你们遇到过没有,函数返回一个函数指针,那么为了继承C语言,返回函数指针的函数是这样写的。
int (*func(int x))(int);
// 该函数是 func(int x) 返回类型是 int(*)(int)
// 即一个函数指针,该函数指针指向的函数
// 返回int,参数是(int)而使用了尾置返回类型,则可以直接如下写
auto func(int x) -> int(*)(int); // 等价上方写法函数参数声明 (C++20)
C++20 起,允许对函数参数声明 auto:
auto func(auto x)
{
return x;
}
// template<typename T> // 上面的函数本质则为该模板
// T func(T x)
// {
// return x;
// }
auto main() -> int
{
func(1); // 调用 int func(int)
func(2.2); // 调用 double func(double)
}类模板实参推导 CTAD (C++17)
C++17 引入了类模板实参推导,意味着你可以编写如下代码:
auto main() -> int
{
int n;
std::cin >> n;
auto a = std::vector(n, 0); // 推导为 std::vector<int>
auto matrix = std::vector(n, std::vector(n, 0)); // 推导为 std::vector<std::vector<int>>
}TIP
STL 很多类在 C++17 都支持了 CTAD,可自行尝试。
尾置类型声明
有人常说,大量使用 auto,看不到变量的类型,降低可读性,其实不然,很多时候使用 auto 只是将类型后置了而已,而有时候那些非后置类型的写法就要做出取舍了,显式写出类型会导致改一处,处处改,而 auto 则可以牵一发而动全身,而有时 auto 不一定会降低可读性。
本节重在说一下尾置类型声明:
// C++
auto add(int x) -> int
{
auto y = int{ x };
// ...
}
auto main() -> int
{
auto a = std::vector(100, 0); // 后置 std::vector<int>
auto y = int{}; // 后置 int, 仅举例,实际不这么写
}// Rust
fn add(x: i32) -> i32
{
let y: i32 = x;
// ...
}# Python
def add(x: int) -> int:
y: int = x
passINFO
尾置类型声明是任何新生语言都采取的做法,在一定程序上可以对齐变量名的声明,让代码的变量声明更加整齐,但由于 C++ 作为非新生语言且有兼容历史的包袱,C++ 是没有尾置返回类型的,auto 的引入一定程度上有了尾置返回类型的雏形。