您的位置:首页 > 编程语言 > C语言/C++

Effective Modern C++ 条款5 尽量用auto代替显式类型声明

2016-08-11 19:23 525 查看

用auto代替显示类型声明

先开个小玩笑:

int x;


该死的,我忘记初始化x 了,那么它的值是未定义的。它可能会被编译器初始化为0,这取决于它的声明位置。

我们再看看另一个小玩笑,它用一个迭代器解引用得到的结构去初始化一个局部变量:

template <typename It>
void dwim(It b, It e)
{
while (b != e) {
typename std::iterator_traits<It>::value_type currValue
= *b;
...
}
}


额,
"typename std::iterator_traits<It>::value_type"
这个表达式可以得到指针或者迭代器所指对象的类型?真的是这样子的吗?我都忘记了,该死!

好吧。最后一个小玩笑,如果我们能用一个闭包类型声明变量会有多么愉悦啊。哦噢,闭包类型只有编译器知道,我们根本写不了那种类型。该死!

该死,该死,该死。用C++编程根本就不是一件开心的事情- -。

好吧,过去C++编程的确不是,但是,在C++11中,所有这些问题都被一扫而空,因为autoauto声明的变量通过初始值来推断类型,所以它们必须是初始化。这意味着你可以告别未初始化变量而产生的问题了:

int x;    // 可能未初始化

auto x2; // 报错,要求初始化

auto x3 = 0;   // 很好,x3已定义


就算用auto声明迭代器解引用,也可以工作得很好:

template <typename It>
void dwim(It b, It e)   // 效果如前
{
while (b != e) {
auto currValue = *b;
...
}
}


然后因为auto用的是类型推断(见条款2),所以它也可以用来表示只有编译器知道的类型:

auto derefUPLess =      // 比较unique_pt指向的Widget
[](const std::unique_ptr<Widget> &p1,
const std::unique_ptr<Widget> &p2)
{  return *p1 < *p2; };


这太爽了。在C++14中,更爽,因为lambda表达式中的参数可以使用auto

auto derefLess =      // 比较具有指针行为的变量指向的值
[](const auto &p1, const auto &p2)
{ return *p1 < *p2; };


尽管这个很爽,但是你可能认为我们并不需要auto来声明一个闭包,因为我们可以使用std::function对象。虽然那也是可以的,但是它的行为并不是你想的那样的。不过现在你更可能在想什么是std::function对象,现在我们理清它。

std::function对象是C++11的标准库的一个模板,它的产生源于函数指针。不过函数指针只可以指向函数,而std::function可以用于所有可调用对象。就像你声明函数指针一样说明函数的类型,std::function对象也要求说明你想使用的那个可调用对象类型。你需要实例化std::function来指定类型。例如,你想要声明一个名字为func 的std::function对象,它适用于类似以下签名的可调用对象:

bool(const std::unique_str<Widget> &, const std::unique_str<Widget> &)


那么你可以这样:

std::function<bool(const std::unique_str<Widget> &,
const std::unique_str<Widget> &)> func;


因为lambda表达式是一个可调用对象,所以闭包可以存储在std::function对象中。这意味着在C++11中,我们可以声明一个不用auto版本的derefUPLess函数:

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


就算我们可以用类型别名来解决语法上的啰嗦,但是使用std::function对象和使用auto是不一样的。一个auto类型推断的变量持有闭包并且它的类型就是闭包的类型,所以它占用的内存只是闭包需要的内存。而std::function声明的变量通过实例化模板而持有闭包,那么它的大小是固定的因为一些指定的签名(has a fixed size for any given signature)。闭包请求的内存大小可能大于std::function指定的内存大小,在这种情况下,std::function的构造函数会请求堆内存来存储闭包。这样的结果就是,std::function对象使用的内存总是比auto类型推断的对象多。而且,std::function由于其实现细节而限制了内联,并且是间接函数调用,所以几乎可以肯定的是,通过std::function对象调用闭包会比auto类型推断的对象中调用要慢。总的来说,std::function方法比起auto方法,消耗内存更大,速度更慢,而且std::function方法可能产生out-of-memory异常(从而请求堆内存?)。还有,你看上面的代码,相比于std::function实例化,用auto实现可以少写很多代码。在std::functionauto的对比中,auto更好。

auto的好处除了可以避免为初始化变量、冗长的类型声明、有能力直接持有闭包之外,还有一个就是避免类型截断这个问题,下面这段代码你很可能见过,甚至写过:

std::vector<int> v;
...
unsigned sz = v.size();


v.size()
返回的真正类型是
std::vector<int>::size_type
,但是很少开发者知道。
std::vector<int>::size_type
被说明为一个无符号整型数,所以很多开发者认为
unsigned
合适然后写下上面的代码。这段代码可能引起有趣的后果。例如在32位的Window系统中,
unsigned
std::vector<int>::size_type
两个类型的位数相同,但是在64位Window系统中,
unsigned
是32位,而
std::vector<int>::size_type
是64位,这意味着在32位Window中可以正常工作,在64为Window中结果可能不正确,而当你把应用程序从32位系统部署到64位系统时,谁想花时间修改这样的bug?

使用auto确保不会发生上面的问题:

auto sz = v.size();
// sz的类型是
std::vector<int>::size_type


如果你仍然不觉得使用auto是明智的,考虑下面的代码:

std::unordered_map<std::string,int> m;
...
for (const std::pair<std::string, int> &p : m)
{
...
}


这看起来很完美,但是有个问题,你发现了吗?

想要知道哪里错了,这要求记住
std::unordered_map
key部分的类型是const修饰的,所以哈希表(
std::unordered_map
)中的
std::pair
类型不是
std::pair<std::string,int>
,而是
std::pair<const std::string, int>
。所以上面的代码是有问题的,而导致的结果是,编译器会把
std::pair<const std::string,int>
对象强制转换为
std::pair<std::string,int>
对象(也就是p声明的类型)。因此编译器为m 哈希表中每一个元素的拷贝生成一个临时对象(key为非const的pair类型),然后p 就引用了那些临时对象。当循环结束,那些临时对象被析构。如果你写了这段代码,你会对这个行为感到惊讶,因为你毫无疑问地想要用p 引用绑定m 中的每一个元素。

ps:本人在gcc环境下测试上面的代码无法通过编译

这种无心的类型错误总是可以用auto避免:

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


这不仅提高效率,而且更容易类型正确。而且,这代码中如果你取p 的地址,有个很吸引人的特点,你取得的地址肯定是指向m 中的元素,而如果代码中不用auto,你取得的地址指向的是一个临时对象,在循环结束后析构。

ps:本人在gcc环境下测试,就算不用auto,指针也是指向m中的元素,并非临时变量。

回顾最后的两个例子,当我们应该写
std::vector<int>::size_type
类型时写了
unsigned
类型和当我们应该写
std::pair<const std::string, int>
类型时写了
std::pair<std::string,int>
类型,都展示了显示类型声明可能会导致你想不到的隐式类型转换。如果你用auto作为类型用于目标变量,你就不需要担心你的用于初始化的变量类型与声明的变量类型不匹配。

这就是建议尽量用auto代替显式类型声明的原因。但是,auto并不是完美的,每个auto变量的类型都是通过初始值或者初始化表达式来推断的,但有些是初始化表达式的类型是你意想不到的。这些例子在条款2和条款6中可以看到,所以这里就不写了。与之替代的是,我把注意力放在另一个概念上,它关于用auto代替显示类型声明,这就是源代码的可读性。

首先,深呼吸一口气,慢慢放松。auto只是一个选择,而不是强迫你使用它。如果基于你的专业判断,你的代码使用显示类型声明会更简洁和更具有维护性,那么你当然可以继续使用显示类型声明。但是需要记住C++并没有开垦新特性,而是采用其他编程语言具有的类型推理(type inference),其他静态类型命令式语言(如C#,D,Scalar,VB等)或多或少具有该特性,刚不用说许多的静态类型函数式语言(如ML,Haskell,OCamal等)。在某种程度上,这是由于Perl,Python和Ruby这些显示类型很少的动态类型语言的成功。在软件开发社区对于类型推理有非常多的经验,这说明这种技术与创建大型的,可维护的,健壮性强的程序没有矛盾之处。

一些开发者可能会为使用auto感到困扰,因为无法通过快速浏览源代码知道一个对象的类型,但是IDE可以缓解这个问题(具体问题见条款4),很多情况下IDE可以为对象的确切类型提供个大概,这在很多时候是已经足够了。例如我们只需要知道一个对象是容器,计数器,智能指针就够了,而无须知道他们的确切类型。

总结

事实上,显式声明对象很多时候会造成微妙的错误,不正确且没效率。而且,如果改变了初始表达式的类型,auto类型也会自动地改变,这意味着对重构(refectoring)有很大帮助。例如你写了个返回类型为int的函数,后来觉得long类型更好,那么你修改了函数后,下一次编译后用auto接收函数返回值的变量会自动更新类型。如果你用int接收,则需要修改每一处函数调用的代码。

需要记住的2点:

auto变量必须初始化,它通常不会类型不匹配,从而刚轻便和更高效,还能减少重构的工作量,一般我们尽量用auto代替显式类型声明。

auto类型变量会有条款2和条款6中的陷阱。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  c++
相关文章推荐