在C ++中用auto声明变量是否有缺点?


143

似乎auto在C ++ 11中添加了相当重要的功能,该功能似乎遵循了许多较新的语言。与Python之类的语言一样,我没有看到任何显式的变量声明(我不确定是否可以使用Python标准)。

使用auto声明变量而不是显式声明变量是否有缺点?



3
我发现的唯一缺点是,当我不得不将代码库移植到(控制台)平台时,该平台的编译器不支持(也无意支持)C ++ 11功能!
山姆

7
只是为了完整性GotW#94 “几乎总是自动”:herbsutter.com/2013/08/12/...
理查德Critten

1
我在听cppcast时,提到了冲突黑白自动和列表初始化程序。我将尝试查找该播客。
Abhinav Gauniyal '16

2
我认为第一个缺点会影响代码的可读性
ggrr

Answers:


111

您只问过缺点,所以我重点介绍其中一些。如果使用得当,auto也具有几个优点。缺点来自易于滥用,以及代码以意想不到的方式行为的可能性增加。

主要缺点是,使用时auto,您不一定知道要创建的对象的类型。在某些情况下,程序员可能希望编译器推断出一种类型,但是编译器坚决推断出另一种类型。

给像这样的声明

auto result = CallSomeFunction(x,y,z);

您不一定了解什么是类型result。可能是一个int。它可能是一个指针。可能还有别的东西。所有这些都支持不同的操作。您还可以通过较小的更改来显着更改代码,例如

auto result = CallSomeFunction(a,y,z);

因为根据CallSomeFunction()结果的不同,结果的类型可能完全不同,因此后续代码的行为可能与预期的完全不同。您可能会在以后的代码中突然触发错误消息(例如,随后尝试取消引用an int,尝试更改now const)。更危险的更改是您的更改会越过编译器,但是后续代码的行为却不同且未知(可能有错误)。

因此,如果没有明确了解某些变量的类型,就很难严格地证明代码按预期工作。这意味着需要更多的努力来证明在高关键性(例如安全性或任务关键性)域中“适合目标”的主张是正确的。

另一个较常见的缺点是,诱惑程序员使用auto钝器来强迫代码进行编译,而不是思考代码的作用并努力使其正确。


58
有趣的是,如果此类示例是使用的缺点auto,那么大多数鸭子式语言在设计上都会遭受此类缺点!
Leben Asa

11
如果CallSomeFunction()根据其参数的顺序返回不同的类型,则这是的设计缺陷CallSomeFunction(),而不是的问题auto。如果您在使用某个函数之前没有阅读该函数的文档,那是程序员的缺陷,而不是的问题auto。-但我知道您在这里扮演恶魔的拥护者,只是Nir Friedman有更好的案例。
DevSolar

16
@DevSolar:为什么会有T CallSomeFunction(T, int, int)设计缺陷?显然,它“根据其参数顺序返回不同的类型”。
MSalters

9
“主要缺点是,通过使用auto,您不一定知道所创建对象的类型。” 您能否详细说明为什么这是问题auto,而不是子表达式临时对象的问题?为什么auto result = foo();不好,但foo().bar()不是呢?
Angew不再为SO

24
从评论看来,“缺点”被解释为某些不可接受的原因。语言功能的缺点是开发人员需要考虑并证明接受或不接受的缺点,即在工程上进行权衡。我并未就为何或不应该使用该功能发表过明确的声明。
彼得

76

auto从原则上讲,这并不是一个缺点,但实际上对某些人来说,这是一个问题。基本上,有些人要么:a)auto视作类型的救世主,在使用它时就闭上了大脑,或者b)忘记了auto总是推论为价值的类型。这导致人们执行以下操作:

auto x = my_obj.method_that_returns_reference();

糟糕,我们只是复制了一些对象。通常是错误或性能失败。然后,您也可以用其他方式摆动:

const auto& stuff = *func_that_returns_unique_ptr();

现在,您会得到一个悬而未决的参考。这些问题根本不是由auto所有问题引起的,因此我不认为它们是反对该问题的合理论据。但是auto,由于我在开头列出的原因,这似乎使这些问题更普遍(根据我的个人经验)。

我认为,随着时间的流逝,人们会调整并理解分工:auto推论出底层的类型,但是您仍然想考虑参考性和常量性。但这需要一些时间。


为什么要开始深复制昂贵的对象?
Laurent LA RIZZA '16

3
@LaurentLARIZZA:有些类之所以具有复制构造函数,仅仅是因为有时需要它们(例如的实例std::vector)。复制昂贵不是类的属性,而是单个对象的属性。因此,method_that_returns_reference可能会引用具有复制构造函数的类的对象,但是复制该对象的成本很高(并且无法从中移出)。
Marc van Leeuwen

@MarcvanLeeuwen:如果复制该对象的成本很高,并且无法移动该对象,为什么将其存储在中std::vector?(因为可能,是的,或者是因为您不控制类,但这不是重点),如果复制昂贵(并且不拥有资源,因为它是可复制的),为什么不对对象使用COW?数据局部性已被对象的大小杀死。
Laurent LA RIZZA '16

2
@LaurentLARIZZA不是存储在向量中的东西的实例昂贵,只是常规的例如vector <double>复制起来很昂贵,它是堆分配+ O(N)的工作。搬家是一条红鲱鱼。我显示的第一行将复制而不移动,除非返回的引用是右值引用。COW确实不在这里或那里。事实是,复制对象总是很昂贵。
尼尔·弗里德曼

4
@Yakk无法安全地执行此操作,因为它可以切片。它唯一可以做的就是= delete过载。虽然更一般地说,您说的是解决方案。如果您感兴趣,这是我探讨的主题:nirfriedman.com/2016/01/18/…
尼尔·弗里德曼

51

其他答案则提到了诸如“您真的不知道变量的类型是什么”之类的缺点。我想说这很大程度上与代码中的草率命名约定有关。如果您的接口名称明确,则无需关心确切的类型。当然,auto result = callSomeFunction(a, b);不会告诉您太多。但是auto valid = isValid(xmlFile, schema);告诉您足够使用,valid而不必关心其确切类型是什么。毕竟,只有just if (callSomeFunction(a, b)),您也不知道类型。与任何其他子表达式临时对象相同。因此,我认为这不是的真正缺点auto

我要说的主要缺点是,有时候,确切的返回类型不是您想要使用的类型。实际上,有时实际返回类型在实现/优化方面与“逻辑”返回类型有所不同。表达式模板是一个很好的例子。假设我们有这个:

SomeType operator* (const Matrix &lhs, const Vector &rhs);

从逻辑上讲,我们希望SomeTypeVector,并且我们绝对希望在代码中将其视为。但是,出于优化目的,我们使用的代数库有可能实现表达式模板,而实际的返回类型是:

MultExpression<Matrix, Vector> operator* (const Matrix &lhs, const Vector &rhs);

现在的问题是,MultExpression<Matrix, Vector>很可能将a const Matrix&const Vector&内部存储;它期望它将Vector在完整表达式结束之前转换为a 。如果我们有此代码,一切都很好:

extern Matrix a, b, c;
extern Vector v;

void compute()
{
  Vector res = a * (b * (c * v));
  // do something with res
}

但是,如果我们在auto这里使用过,可能会遇到麻烦:

void compute()
{
  auto res = a * (b * (c * v));
  // Oops! Now `res` is referring to temporaries (such as (c * v)) which no longer exist
}

3
@NirFriedman是的,它很强大,但是我实际上感觉到它auto几乎没有缺点,所以我坚持这种实力。代理等的其他示例包括各种“字符串生成器”和在DSL中发现的类似对象。
Angew不再为SO

2
auto以前,我一直被表达式模板所困扰,尤其是对于Eigen库。这特别棘手,因为问题通常不会在调试版本中出现。
丹丹

1
使用Armadillo矩阵库时,auto也会使用,这会大量使用模板元编程来实现优化目的。幸运的是,开发人员已经添加了.eval()函数,该函数可用于避免与auto
–mtall相关

2
“如果接口名称明确,则不必关心确切的类型是什么。”您的编译器无法通过研究变量的名称来检查代码的正确性。这是类型系统的重点。盲目绕过它是愚蠢的!
Lightness Races in Orbit

1
@Angew:对于临时人员而言,这不是问题,因为您通常会立即使用它们,而这些临时操作通常不auto涉及某种类型检查(并且auto随处飞溅都会使该类型安全性消失,就像在其他地方一样)。这不是一个很好的比较。
Lightness Races in Orbit

13

唯一的缺点是,有时你不能声明const_iteratorauto。在这个问题的代码示例中,您将获得普通的(非const)迭代器:

map<string,int> usa;
//...init usa
auto city_it = usa.find("New York");

3
好吧,iterator无论如何您都会得到一个,因为您的地图不是const。如果要将其转换为const_iterator,则可以照常显式指定变量类型,或者提取一个方法,以使map在您的上下文中为const find。(我希望后者是SRP。)
Laurent LA RIZZA

auto city_it = static_cast<const auto&>(map).find("New York")?或者,对于C ++ 17 ,auto city_if = std::as_const(map).find("New York")
Dev Null

11

它使您的代码更难阅读或繁琐。想象这样的事情:

auto output = doSomethingWithData(variables);

现在,要弄清楚输出的类型,您必须跟踪doSomethingWithData功能的签名。


40
不总是。auto it = vec.begin();比起阅读起来容易得多std::vector<std::wstring>::iterator it = vec.begin();
乔纳森·波特

4
同意 这取决于用例。我本可以更精确一些。
Skam,2016年

1
@SeeDart是的,像这样使用auto的人做错了。
lciamp '16

6
“跟踪功能签名”,如果不是鼠标悬停或按键按下(“跟随符号” /“转到声明” /无论调用什么),则需要配置更多的编辑器或切换到无需配置即可执行此操作的IDE ...不过,您的观点仍然有效。
海德

6
我注意到在检查签入时,这不是在IDE中,而是在比较小的窥视孔中!因此,使用auto很难阅读。
JDługosz

10

这个开发人员一样,我讨厌auto。或更确切地说,我讨厌人们如何滥用auto

我(强烈)认为这auto是为了帮助您编写通用代码,而不是减少输入
C ++是一种语言,其目标是让您编写健壮的代码,而不是减少开发时间。
从C ++的许多功能中可以很明显地auto看出这一点,但是不幸的是,一些新的功能(例如减少打字)会误导人们以为他们应该开始对打字变得懒惰。

在早期auto,人们使用typedefs,这很棒,因为它typedef 允许库的设计者帮助您弄清楚返回类型应该是什么,以便他们的库按预期工作。当使用时auto,您从类的设计者那里取消了该控件,而是要求编译器确定类型是什么,这从工具箱中删除了功能最强大的C ++工具之一,并有可能破坏其代码。

通常,如果使用auto,那应该是因为您的代码适用于任何合理的类型而不是因为您懒得写下它应该使用的类型。如果您auto用作帮助懒惰的工具,那么将会发生的事情是您最终开始在程序中引入一些细微的错误,通常是由于您使用而不发生隐式转换而导致的auto

不幸的是,这些错误很难在此处的简短示例中进行说明,因为它们的简洁性使其比用户项目中的实际示例更具说服力;但是,它们很容易在大量模板代码中发生,这些代码期望某些隐式转换能够进行地点。

如果你想要一个例子,有一个在这里。不过,需要注意一点:在尝试跳转并批评代码之前:请记住,围绕此类隐式转换已开发了许多知名且成熟的库,它们之所以存在是因为它们解决了即使不是不可能的困难的问题。否则解决。批评之前尝试找出更好的解决方案


3
which was great because typedef allowed the designer of the library to help you figure out what the return type should be, so that their library works as expected. When you use auto, you take away that control from the class's designer and instead ask the compiler to figure out what the type should beIMO并不是一个很好的理由。最新的IDE(例如Visual Studio 2015)允许您将鼠标悬停在上来检查变量的类型auto。这是* *完全相同一样的typedef一个。
草帽鸡

@JameyD:您在这里缺少几个关键点:(1)您的IDE参数仅在类型是具体的而不是模板的情况下有效。对于从属类型,IDE可能无法告诉您正确的类型,例如typename std::iterator_traits<It>::value_type。(2)整个点是,推断出类型需要是“完全相同”作为正确的类型意在通过代码的先前设计师; 通过使用auto,您将失去设计师指定正确类型的能力。
user541686 '16

您基本上是在谈论代理,答案之一已经提到。对于大多数人来说,表达式模板和vector <bool>废话并不是日常代码。在大多数情况下,您不需要隐式转换,自动转换可以帮助您。赫伯·萨特(Herb Sutter)在他的一篇博客文章中广泛讨论了auto的优势,并且不仅主要涉及击键,而且还不仅仅是针对通用代码。另外,您提供的第一个链接是该博客文章,这只是一个糟糕的建议(这就是他在评论部分遭到批评的原因)。
弗里德曼

@NirFriedman:“…… vector<bool>废话” ……赦免?您如何bitset实施?还是您认为位容器完全是胡说八道?
user541686 '16

1
@NirFriedman:关于vector <bool>的一切对我来说都不是新闻。我要告诉你的是,而你公然拒绝理解的是,就此问题而言,位集与vector <bool>没什么不同- 两者都使用代理,因为代理被认为是有用的,而且代理有用的事实是您需要接受的现实,而不是生活在否认之中。您能否停止将其变成关于您认为代理是否有用的辩论?这不是辩论的主题,而且,您对它们的观点只是您的观点,不是某种事实。
user541686 '16

6

auto没有缺点本身,我提倡(手波浪)在新代码中任何地方使用它。它使您的代码能够始终进行类型检查,并始终避免进行静默切片。(如果B从派生A而返回的函数A突然返回B,则auto表现为预期的存储其返回值)

虽然,C ++ 11之前的旧版代码可能依赖于使用显式类型的变量引起的隐式转换。将显式类型的变量auto更改为可能会改变代码的行为,因此您最好保持谨慎。


拒绝投票是公平的,但是请您评论一下原因?
洛朗LA RIZZA,2016年

我没有对您投反对票,但是auto它本身有缺点(或者至少-很多认为它有缺点)。考虑一下与Sutter,Alexandrescu和Meyers进行的小组讨论中的第二个问题中给出的示例:如果您拥有auto x = foo(); if (x) { bar(); } else { baz(); }foo()返回bool-如果foo()更改返回一个枚举(三个选项而不是两个),会发生什么?该auto代码将继续工作,但会产生意外的结果。
einpoklum

@einpoklum:在没有范围的枚举的情况下,使用bool而不是auto更改任何内容吗?我可能是错的(无法在此处检查),但我认为唯一的区别是要bool在变量声明而不是在条件中求值时进行转换if。如果enum作用域是范围,则在bool没有明确通知的情况下不得进行转换。
洛朗LA RIZZA

4

关键字auto只是根据返回值推断出类型。因此,它与Python对象不同,例如

# Python
a
a = 10       # OK
a = "10"     # OK
a = ClassA() # OK

// C++
auto a;      // Unable to deduce variable a
auto a = 10; // OK
a = "10";    // Value of const char* can't be assigned to int
a = ClassA{} // Value of ClassA can't be assigned to int
a = 10.0;    // OK, implicit casting warning

由于auto是在编译过程中得出的,因此在运行时不会有任何缺点。


1
是的,它基本上可以完成type()python中的工作。它推导类型,但不创建该类型的新变量。
lciamp '16

2
@lciamp实际上,那是decltypeauto专门用于变量分配。
立方

4

如果您问我,到目前为止,这里没有人提到,但就其本身而言,是值得回答的。

由于(即使每个人都应该意识到C != C++)用C编写的代码可以轻松地设计为C ++代码提供基础,因此无需过多的努力就可以与C ++兼容,因此这是设计的要求。

我知道一些规则,其中一些定义良好的构造C对其无效,C++反之亦然。但是,这只会导致可执行文件损坏,并且会应用已知的UB子句,大多数情况下,这种奇怪的循环会导致崩溃或发生任何事情(甚至可能使其未被发现,但这并不重要)。

但是auto是第一次1这个变化!

假设您之前曾经auto用作存储类说明符,然后传输了代码。它甚至不一定(取决于使用方式)“中断”;实际上,它可以静默更改程序的行为。

这是应该牢记的。


1 至少是我第一次知道。


1
无论如何,在尝试编译时都会出现编译器错误。
草帽鸡

@JameyD:怎么办?为什么两个具有不同含义的有效代码情况都会出错?
dhein

8
如果您int在C语言中依赖“没有类型暗含”,那么您应该从中得到所有不好的东西。而且,如果您不依赖它,将auto与类型一起用作存储类说明符会在C ++中给您带来不错的编译错误(在这种情况下,这是一件好事)。
Angew不再为SO

1
@Angew好,这就是我指的情况,是的。不是这样 但这至少应牢记。
dhein

3

我能想到的一个原因是,您失去了强迫返回的班级的机会。如果您的函数或方法返回一个长64位,而您只想要一个32无符号的int,那么您将失去控制它的机会。


1
有static_cast,例如IIRC,例如Meyers的Effective Modern C ++,甚至建议使用它来指定自动类型变量的类型。
海德

2

正如我在此答案中 所描述的,auto有时可能会导致您不想要的时髦情况。您必须明确地说auto&要具有引用类型,而这样做auto只能创建指针类型。通过一起省略说明符,可能导致混淆,从而导致引用的副本而不是实际的引用。


2
那不时髦。就是auto这样,从不推断引用或const类型。作为auto参考,您最好使用auto&&。(通用参考)如果该类型不便宜而不能复制或拥有资源,则该类型一开始就不可复制。
洛朗LA RIZZA,2016年

1

另一个令人讨厌的例子:

for (auto i = 0; i < s.size(); ++i)

生成警告(comparison between signed and unsigned integer expressions [-Wsign-compare]),因为它i是带符号的int。为了避免这种情况,您需要编写例如

for (auto i = 0U; i < s.size(); ++i)

也许更好:

for (auto i = 0ULL; i < s.size(); ++i)

1
是的,我也觉得这很烦人。但是语言上的漏洞还存在于其他地方。为了使此代码真正可移植,并假设size返回size_t,您必须能够拥有一个size_t类似的文字0z。但是您可以声明一个UDL来做到这一点。(size_t operator""_z(...)
Laurent LA RIZZA

1
纯粹是理论上的异议:unsigned很有可能不足以容纳std::size_t主流架构上的所有值,因此,在极少数情况下,如果某人的容器中的元素数量非常庞大,那么使用该容器unsigned可能会在较低范围内造成无限循环指数。尽管这不太可能成为问题,但std::size_t应该使用它来获取可以正确表示意图的干净代码。我不确定即使unsigned long long是严格保证也能满足要求,尽管实际上它必须是相同的。
underscore_d

@underscore_d:是的,很公平- unsigned long long保证至少为64位,但size_t我想理论上可以大于64位。当然,如果您的容器中有> 2 ^ 64个元素,那么您可能有更大的问题要担心... ;-)
Paul R

1

我认为auto在本地化环境中使用是很好的,在这种情况下,读者可以轻松地&显然可以推断出其类型,或者以其类型的注释或可以推断实际类型的名称进行详细记录。那些不了解它如何工作的人可能会以错误的方式使用它,例如使用它代替template或类似方法。我认为这是一些好用例和坏用例。

void test (const int & a)
{
    // b is not const
    // b is not a reference

    auto b = a;

    // b type is decided by the compiler based on value of a
    // a is int
}

善用

迭代器

std::vector<boost::tuple<ClassWithLongName1,std::vector<ClassWithLongName2>,int> v();

..

std::vector<boost::tuple<ClassWithLongName1,std::vector<ClassWithLongName2>,int>::iterator it = v.begin();

// VS

auto vi = v.begin();

功能指针

int test (ClassWithLongName1 a, ClassWithLongName2 b, int c)
{
    ..
}

..

int (*fp)(ClassWithLongName1, ClassWithLongName2, int) = test;

// VS

auto *f = test;

不良用法

数据流

auto input = "";

..

auto output = test(input);

功能签名

auto test (auto a, auto b, auto c)
{
    ..
}

琐事

for(auto i = 0; i < 100; i++)
{
    ..
}

如果需要int,则必须再输入一个字符auto。这是不可接受的
Rerito

@Rerito是的,int在这里很容易看到,而且输入int更短。这就是为什么这是一件小事。
Khaled.K,

0

我很惊讶没有人提及此事,但是假设您正在计算某些因素的阶乘:

#include <iostream>
using namespace std;

int main() {
    auto n = 40;
    auto factorial = 1;

    for(int i = 1; i <=n; ++i)
    {
        factorial *= i;
    }

    cout << "Factorial of " << n << " = " << factorial <<endl;   
    cout << "Size of factorial: " << sizeof(factorial) << endl; 
    return 0;
}

此代码将输出以下内容:

Factorial of 40 = 0
Size of factorial: 4

那绝对不是预期的结果。发生这种情况是因为auto推导了变量阶乘的类型,int因为它被分配给1

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.