写C/C++
的程序员都知道定义一个变量如果没有给初值会导致这个变量的值是未定义的,这往往是bug的源泉,在使用容器的迭代器的时候,需要定义一个迭代器变量,这个变量的类型很冗长,例如下面的代码应该经常可以碰到。
vector<int>::iterator it = xxx.begin();
vector<map<int,int> >::iterator
.....
到了C++11,算是对上面碰到的问题有了一个比较折中的解决方案了,如下:
auto x1; //没有初始化会报错
auto it = xxx.begin(); //没有冗长的类型名了
auto在Item2中介绍过,利用了类型推导实现。除了上面提到的两个优点外,auto还有一些其他的不为人知的优点:
auto f1 = [](const std::unique_ptr<int>& p1,const std::unique_ptr<int>& p2) {
return *p1 < *p2;
};
std::function<bool(const std::unique_ptr<int>&,std::unique_ptr<int>&)> f2 = [](
const std::unique_ptr<int>& p1,const std::unique_ptr<int>& p2) {
return *p1 < *p2;
};
上面的这个场景,使用auto
和std::function
,会有显著的不同,f1的类型就是一个闭包类型,而f2是一个std::function
模板类(可以通过Item4提到的方法来打印输出f1和f2的类型),他内部会使用一块内存来保存闭包,但是这块内存的大小是固定的,因此会随着闭包大小的增大进而会进行内存的分配所以可能会产生内存分配失败的异常,下面是截取了std::function
的部分实现的源码,可读性不佳,不过可以通过注释大致了解这一事实。
// Clone a location-invariant function object that fits within
// an _Any_data structure.
static void
_M_clone(_Any_data& __dest, const _Any_data& __source, true_type)
{
new (__dest._M_access()) _Functor(__source._M_access<_Functor>());
}
// Clone a function object that is not location-invariant or
// that cannot fit into an _Any_data structure.
static void
_M_clone(_Any_data& __dest, const _Any_data& __source, false_type)
{
__dest._M_access<_Functor*>() =
new _Functor(*__source._M_access<_Functor*>());
}
f1
,f2
在运行的时候开销也不同,f1因为是闭包可以直接运行,而f2
其实是间接的调用了内部保存的闭包。因此综合来说auto
更适合,不仅仅代码清晰,开销也小。在C++14
中,lambda
的参数也可以使用auto了,f1
可以简写成如下的形式
auto f1 = [](const auto& p1,const auto& p2) {
return *p1 < *p2;
};
接着让我们来看看另外一个在C++中不为人知的秘密,通过auto可以轻松的避免这类问题的发生。
std::unordered_map<std::string,int> m;
上面是一个key为std::string
,value的类型是int
的unordered_map其实就是一个std::pair<std::string,int>
类型,我想这应该是大多数人都是这么认为的吧,毕竟所见即所得。我们能看见的就是key是std::string
,value是int但是其实它的key是const的。也就是实际上它是std::pair<const std::string,int>
具体的细节可以参见cppreference.因此这带来了一些问题,如下:
for(const std::pair<std::string,int>& p : m) {
//do something
}
相信大多数人都会写出上面的代码,如果不知道map的key其实是const的很难发现上面的问题,p没办法引用m中的元素,为了可以成功运行,编译器通过将std::pair<const std::string,int>
转化成一个临时的std::pair<std::string,int>
对象然后使用p引用这个临时的对象,循环结束后再释放这个临时的对象。这样就造成了大量临时对象不可避免的构造和析构的成本。可以通过下面的方法验证上面的结论:
#include <unordered_map>
#include <iostream>
int main() {
int p;
std::unordered_map<std::string,int> m;
m["test"] = 1;
std::unordered_map<std::string,int>::iterator it = m.find("test");
printf("address: %p\n",*it);
for(const std::pair<std::string,int>&p : m) {
printf("address: %p\n",p);
}
for(const auto&c : m) {
printf("address: %p\n",c);
}
}
你会发现it
和c
的地址是一样,和p
的地址不一样,如果把p的key类型换成const的则it,c,p
的地址都是一样的。像上面这样的不为人知的小例子还有很多,如果你不清楚内幕很容易导致你的代码效率低下,但是如果使用auto就可以在一定程度上避免这些坑。一直在说auto
的优点,现在来说说auto
的一些不足之处吧。在Item2中提到过,auto在一些情况下得到的类型并不是实际的类型,具体可以参考Item2,有的人认为使用auto后会导致了代码不易读,因为不知道变量的实际类型,这个其实在Item4中提到过,可以借助于IDE和其他的一些办法,但是我觉得只要你不滥用auto
即可。