[C++] - auto的使用、优点和缺点

Table of Contents

1.auto的优点

1>避免未初始化变量

2>避免冗长的变量声明

3>具有直接持有闭包(closure)的能力

4>type shortcuts 类型捷径

2.auto的缺点

1>不适用invisible proxy class

2>Braced Initializer 类型推断


1.auto的优点

1>避免未初始化变量

auto变量从它们的初始化式中推断它们的类型,所以它们必须被初始化。这意味着你可以免受很多未初始化变量带来的问题。

int x1;       // potentially uninitialized
auto x2;      // error! initializer required
auto x3 = 0;  // fine, x's value is well-defined

2>避免冗长的变量声明

因为auto使用类型推断,所以它可以表示某些只有编译器知道的类型:

// comparison func for Widgets pointed to by std::unique_ptrs
auto derefUPLess = [](const std::unique_ptr& p1, const std::unique_ptr& p2){
    return *p1 < *p2;
};

在C++14中,甚至允许在lambda表达式参数中使用auto:

// values pointed to by anything pointer-like
auto derefLess = [](const auto& p1, const auto& p2){
    return *p1 < *p2;
};

3>具有直接持有闭包(closure)的能力

std::function是C++11标准库里的一个模板,它将函数指针通用化。函数指针只能指向函数,但是std::function对象可以指向任何callable对象(即可以像函数一样调用)。当你创建一个函数指针时,你必须给出函数的signature(即返回类型和形参表),同样地,当你创建std::function对象时,你也必须指定。

因为lambda表达式产生callable对象,所以闭包(closure)可以被存在std::function对象中。这意味着我们可以声明C++11版本的不使用auto的derefUPLess:

std::function&, const std::unique_ptr&)>
derefUPLess = [](const std::unique_ptr& p1, const std::unique_ptr& p2) {     
    return *p1 < *p2; 
};

即使仅仅是将auto替换为类型名,使用std::function和auto仍然是不同的。

一个持有闭包(closure)的auto变量具有和闭包(closure)一样的类型,并且因此仅消耗闭包(closure)所需求的内存空间。

一个持有闭包(closure)的std::function变量的类型是std::function模板的一个具现(instantiation),并且它对于任意的函数signature都有固定的内存空间。这个内存空间的大小也许并不满足闭包(closure)的需求,所以std::function的构造函数可能会申请堆内存来存储闭包(closure)。因此,std::function对象通常会比auto对象消耗更多的内存空间。

另外,实现细节禁用inline,会导致间接地函数调用。因此,通过std::function对象调用闭包(closure)几乎肯定会比通过auto对象调用慢。

总之,std::function方法会比auto方法消耗更多空间且执行更慢,并且std::function方法还可能产生out-of-memory的异常。

4>type shortcuts 类型捷径

std::vector v;
unsigned sz1 = v.size();
auto sz2 = v.size(); // sz2's type is std::vector::size_type

v.size()的返回类型是std::vector::size_type。std::vector::size_type被指定为是unsigned int类型。所以很多人认为unsigned已经足够好了。在32位机子上,unsigned和std::vector::size_type有相同的大小,但在64位机子上,unsigned是32位,而std::vector::size_type是64位。所以可能在64位机子上会有错误的行为。使用auto就不会有这个问题。

std::unordered_map m;
...

for(const std::pair& p : m)
{
    ... // do something with p
}

for(const auto& p : m)
{
    ... //as before
}

看出第一种写法有什么问题吗?std::unordered_map的key是const的,所以在hash table中的std::pair不是std::pair,而是std::pair。对于第一种写法,编译器会想方设法把std::pair对象转换为std::pair对象。编译器会拷贝m中的每个对象来生成一个临时对象,然后把p绑定到这个临时对象上。在每次循环结束时,临时对象会被销毁。这并不是你所期望的,你期望的是仅仅把p绑定到m中的每一个对象上。使用auto就可以避免这个问题。

这两个例子说明显式给出类型会导致你不期望的隐式转换。如果使用auto作为变量的类型,就不必担心你声明的类型与用来初始化变量的表达式的类型不一致的问题。

2.auto的缺点

1>不适用invisible proxy class

虽然使用auto声明变量会带来很多好处,但是有时auto的类型推断也会带来你不期望的行为。

std::vector features(const Widget& w);
Widget w;
...
bool highPriority = features(w)[5];
...
processWidget(w, highPriority);

这个例子没有问题,可以正常工作。但如果用auto声明highPriority,就会带来未定义的行为。

auto highPriority = features(w)[5];
...
processWidget(w, highPriority); // undefined behavior!

为什么会有未定义的行为?这就要牵涉到vector的实现。

1>首先,使用auto的话,highPriority的类型就不是bool了,而是std::vector::reference(std::vector的内嵌类)。std::vector::operator[]应该返回一个指向容器内元素的引用,但是除bool以外。std::vector里存放的并不是bool对象,而是用1个bit代表1个bool的压缩形式存放。这就导致std::vector::operator[]无法返回一个对元素的引用(因为C++禁止对bit引用),所以只能返回一个行为类似bool&的对象,也就是std::vector::reference。为了能够行为类似bool&,std::vector::reference在所有的上下文中会被隐式转换为bool(注意不是bool&)。

2>std::vector::reference的典型实现是,它包含一个指针,指向内存空间的一个字(word),这个字中的bits被std::vector对象管理。除了指针,它还包含所要访问的bit在这个字中的偏移(offset)。

3>features(w)会返回一个临时的std::vector对象temp,highPriority是features(w)[5]返回的std::vector::reference对象的拷贝。因此highPriority也含有一个指向temp中字的指针和对应bit的偏移。在highPriority的赋值语句结束时,这个临时对象temp被销毁。因此,highPriority中的指针就变成了悬挂指针。这就导致了processWidget中对highPriority的访问会有未定义行为。

std::vector::reference是代理类(proxy class)的一个例子。std::vector::reference提供了一个假象,即std::vector::operator[]返回了一个对bit的引用。标准库中智能指针也是代理类,它们在原始指针上施加资源管理。

一些代理类被设计成显式行为,如std::shared_ptr和std::unique_ptr。其他的代理类则被设计成隐式地,如std::vector::reference,std::bitset::reference。

通常来说,隐式的代理类不适合用auto。所以要避免如下的使用:

auto someVar = expression of "invisible" proxy class type;

2>Braced Initializer 类型推断

如果auto变量使用花括号的初始化式子,变量的类型将被推断为std::initializer_list。

auto x = {11, 23, 9}; // x's type is std::initializer_list

C++14允许auto用于函数的返回类型,并且lambda的参数也可以用auto声明。但是这些对auto的使用并不会将花括号的初始化式子推断为std::initializer_list,因此编译时会报错:

auto createInitList()
{
    return {1, 2, 3}; // error: can't deduce type for {1, 2, 3}
}

auto resetV = [&v](const auto& newValue) { }; // C++14
resetV({1, 2, 3}); // error! can't deduce type for {1, 2, 3}

 

整理自:《Effective Modern C++》第二章 auto

你可能感兴趣的:(C++)