目录
上一篇详细讲解了使用auto关键字进行自动类型推导时的推导规则,这一篇重点讲解auto的使用以及C++14、C++17、C++20等新标准对auto的功能完善,最后再介绍auto的使用限制。上一篇请从这里阅读:深入解析C++ auto自动类型推导(一)
使用auto的好处
- 强制初始化的作用
当你定义一个变量时,可以这样写:
int i;
这样写编译是能够通过的,但是却有安全隐患,比如在局部代码中定义了这个变量,然后又接着使用它了,可能面临未初始化的风险。但如果你这样写:
auto i;
这样是编译不通过的,因为变量i缺少初始值,你必须给i指定初始值,如下:
auto i = 0;
必须给变量i初始值才能编译通过,这就避免了使用未初始化变量的风险。
- 定义小范围内的局部变量时
在小范围的局部代码中定义一个临时变量,对理解整体代码不会造成困扰的,比如:
for (auto i = 1; i < size(); ++i) {}
或者是基于范围的for循环的代码,只是想要遍历容器中的元素,对于元素的类型不关心,如:
std::vector<int> v = {};
for (const auto& i : v) {}
- 减少冗余代码
当变量的类型非常长时,明确写出它的类型会使代码变得又臃肿又难懂,而实际上我们并不关心它的具体类型,如:
std::map<std::string, int> m;
for (std::map<std::string, int>::iterator it = m.begin(); it != m.end(); ++it) {}
上面的代码非常长,造成阅读代码的不便,对增加理解代码的逻辑也没有什么好处,实际上我们并不关心it的实际类型,这时使用auto就使代码变得简洁:
for (auto it = m.begin(); it != m.end(); ++it) {}
再比如下面的例子:
std::unordered_multimap<int, int> m;
std::pair<std::unordered_multimap<int, int>::iterator,
std::unordered_multimap<int ,int>::iterator>
range = m.equal_range(k);
对于上面的代码简直难懂,第一遍看还看不出来想代表的意思是什么,如果改为auto来写,则一目了然,一看就知道是在定义一个变量:
auto range = m.equal_range(k);
- 无法写出的类型
如果说上面的代码虽然难懂和难写,毕竟还可以写出来,但有时在某些情况下却无法写出来,比如用一个变量来存储lambda表达式时,我们无法写出lambda表达式的类型是什么,这时可以使用auto来自动推导:
auto compare = [](int p1, int p2) { return p1 < p2; }
- 避免对类型硬编码
除了上面提到的可以减少代码的冗余之外,使用auto也可以避免对类型的硬编码,也就是说不写死变量的类型,让编译器自动推导,如果我们要修改代码,就不用去修改相应的类型,比如我们将一种容器的类型改为另一种容器,迭代器的类型不需要修改,如:
std::map<std::string, int> m = { ... };
auto it = m.begin();
// 修改为无序容器时
std::unordered_map<std::string, int> m = { ... };
auto it = m.begin();
C++标准库里的容器大部分的接口都是相同的,泛型算法也能应用于大部分的容器,所以对于容器的具体类型并不是很重要,当根据业务的需要更换不同的容器时,使用auto可以很方便的修改代码。
- 跨平台可移植性
假如你的代码中定义了一个vector,然后想要获取vector的元素的大小,这时你调用了成员函数size来获取,此时应该定义一个什么类型的变量来承接它的返回值?vector的成员函数size的原型如下:
size_type size() const noexcept;
size_type是vector内定义的类型,标准库对它的解释是“an unsigned integral type that can represent any non-negative value of difference_type”,于是你认为用unsigned类型就可以了,于是写下如下代码:
std::vector<int> v;
unsigned sz = v.size();
这样写可能会导致安全隐患,比如在32位的系统上,unsigned的大小是4个字节,size_type的大小也是4个字节,但是在64位的系统上,unsigned的大小是4个字节,而size_type的大小却是8个字节。这意味着原本在32位系统上运行良好的代码可能在64位的系统上运行异常,如果这里用auto来定义变量,则可以避免这种问题。
- 避免写错类型
还有一种似是而非的问题,就是你的代码看起来没有问题,编译也没有问题,运行也正常,但是效率可能不如预期的高,比如有以下的代码:
std::unordered_map<std::string, int> m = { ... };
for (const std::pair<std::string, int> &p : m) {}
这段代码看起来完全没有问题,编译也没有任何警告,但是却暗藏隐患。原因是std::unordered_map容器的键值的类型是const的,所以std::pair的类型不是std::pair<std::string, int>而是std::pair<const std::string, int>。但是上面的代码中定义p的类型是前者,这会导致编译器想尽办法来将m中的元素(类型为std::pair<const std::string, int>)转换成std::pair<std::string, int>类型,因此编译器会拷贝m中的所有元素到临时对象,然后再让p引用到这些临时对象,每迭代一次,临时对象就被析构一次,这就导致了无故拷贝了那么多次对象和析构临时对象,效率上当然会大打折扣。如果你用auto来替代上面的定义,则完全可以避免这样的问题发生,如:
for (const auto& p : m) {}
新标准新增功能
- 自动推导函数的返回值类型(C++14)
C++14标准支持了使用auto来推导函数的返回值类型,这样就不必明确写出函数返回值的类型,如下的代码:
template<typename T1, typename T2>
auto add(T1 a, T2 b) {
return a + b;
}
int main() {
auto i = add(1, 2);
}
不用管传入给add函数的参数的类型是什么,编译器会自动推导出返回值的类型。
- 使用auto声明lambda的形参(C++14)
C++14标准还支持了可以使用auto来声明lambda表达式的形参,但普通函数的形参使用auto来声明需要C++20标准才支持,下面会提到。如下面的例子:
auto sum = [](auto p1, auto p2) { return p1 + p2; };
这样定义的lambda式有点像是模板,调用sum时会根据传入的参数推导出类型,你可以传入int类型参数也可以传入double类型参数,甚至也可以传入自定义类型,如果自定义类型支持加法运算的话。
- 非类型模板形参的占位符(C++17)
C++17标准再次拓展了auto的功能,使得能够作为非类型模板形参的占位符,如下的例子:
template<auto N>
void func() {
std::cout << N << std::endl;
}
func<1>(); // N为int类型
func<'c'>(); // N为chat类型
但是要保证推导出来的类型是能够作为模板形参的,比如推导出来是double类型,但模板参数不能接受是double类型时,则会导致编译不通过。
- 结构化绑定功能(C++17)
C++17标准中auto还支持了结构化绑定的功能,这个功能有点类似tuple类型的tie函数,它可以分解结构化类型的数据,把多个变量绑定到结构化对象内部的对象上,在没有支持这个功能之前,要分解tuple里的数据需要这样写:
tuple x{1, "hello"s, 5.0};
itn a;
std::string b;
double c;
std::tie(a, b, c) = x; // a=1, b="hello", c=5.0
在C++17之后可以使用auto来这样写:
tuple x{1, "hello"s, 5.0};
auto [a, b, c] = x; // 作用如上
std::cout << "a=" << a << ", b=" << b << ", c=" << c << std::endl;
auto的推导功能从以前对单个变量进行类型推导扩展到可以对一组变量的推导,这样可以让我们省略了需要先声明变量再处理结构化对象的麻烦,特别是在for循环中遍历容器时,如下:
std::map<std::string, int> m;
for (auto& [k, v] : m) {
std::cout << k << " => " << v << std::endl;
}
- 使用auto声明函数的形参(C++20)
之前提到无法在普通函数中使用auto来声明形参,这个功能在C++20中也得到了支持。你终于可以写下这样的代码了:
auto add (auto p1, auto p2) { return p1 + p2; };
auto i = add(1, 2);
auto d = add(5.0, 6.0);
auto s = add("hello"s, "world"s); // 必须要写上s,表示是string类型,默认是const char*,
// char*类型是不支持加法的
这个看起来是不是和模板很像?但是写法要比模板要简单,通过查看生成的汇编代码,看到编译器的处理方式跟模板的处理方式是一样的,也就是说上面的三个函数调用分别产生出了三个函数实例:
auto add<int, int>(int, int);
auto add<double, double>(double, double);
auto add<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >);
使用auto的限制
上面详细列出了使用auto的好处和使用场景,但在有些地方使用auto还存在限制,下面也一并罗列出来。
- 类内初始化成员时不能使用auto
在C++11标准中已经支持了在类内初始化数据成员,也就是说在定义类时,可以直接在类内声明数据成员的地方直接写上它们的初始值,但是在这个情况下不能使用auto来声明非静态数据成员,比如:
class Object {
auto a = 1; // 编译错误。
};
上面的代码会出现编译错误:error: 'auto' not allowed in non-static class member。虽然不能支持声明非静态数据成员,但却可以支持声明静态数据成员,在C++17标准之前,使用auto声明静态数据成员需要加上const修饰词,这就给使用上造成了不便,因此在C++17标准中取消了这个限制:
class Object {
static inline auto a = 1; // 需要写上inline修饰词
};
- 函数无法返回initializer_list类型
虽然在C++14中支持了自动推导函数的返回值类型,但却不支持返回的类型是initializer_list<T>类型,因此下面的代码将编译不通过:
auto createList() {
return {1, 2, 3};
}
编译错误信息:error: cannot deduce return type from initializer list。
- lambda式参数无法使用initializer_list类型
同样地,在lambda式使用auto来声明形参时,也不能给它传递initializer_list<T>类型的参数,如下代码:
std::vector<int> v;
auto resetV = [&v](const auto& newV) { v = newV; };
resetV({1, 2, 3});
上面的代码会编译错误,无法使用参数{1, 2, 3}来推导出newV的类型。
本主页会定期更新,为了能够及时获得更新,敬请关注我:点击左下角的关注。也可以关注公众号:请在微信上搜索公众号“AI与编程之窗”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。