C++11中的auto关键字使用过度会有什么问题?

256

我一直在使用C++11标准中提供的新关键字auto来处理复杂的模板类型,我认为这就是它的设计初衷。但我也在将其用于以下情况:

auto foo = std::make_shared<Foo>();

更加怀疑的是:

auto foo = bla(); // where bla() return a shared_ptr<Foo>

我还没有看到关于这个话题的太多讨论。似乎auto可能被过度使用,因为类型通常是文档和健全性检查的一种形式。在使用auto时,你会在哪里划线,以及对于这个新特性的推荐用例是什么?

澄清一下:我不是在寻求哲学上的观点;我正在询问标准委员会对这个关键字的预期用途,可能还有关于如何在实践中实现这个预期用途的评论。


19
@heishe,我添加了一个澄清。如果您非常笼统地阅读问题,则似乎在询问主观意见,但实际上,如果您使用了 auto 关键字,则应该知道它的正确使用方式。作为一个新手,我想知道应该如何使用它? - Alan Turing
14
当C#引入了var关键字后,我在很多地方看到过有关此话题的讨论(也就是当人们克服了它并非动态类型的想法后)。如果你愿意,可以从这个问题开始,并查看相关问题。 - R. Martinho Fernandes
3
@ildjarn,很抱歉你有这样的感受。我选择的答案列出了auto的良好和不良使用的清晰代码片段。并非所有问题都是问2+2等简单问题。例如,询问如何计算2+2仍然是一个好问题,但你可以轻易地称之为“主观”。也许我太天真了,但对我来说,如果一个问题导致了一个有用的澄清回答,那么问题和答案都应该在网站上。 - Alan Turing
3
@Lex:无论什么东西都要么是合法的,要么不合法;如果将一些合法的事情称为“不好”,那就是主观的定义。例如,称auto foo = bla();为“不好”显然是一种观点,而不是事实,这使得这个问题和答案成为了一个讨论,这也使得它与Programmers SE相关,这正是关闭投票所表明的。/耸肩 - ildjarn
4
Herb Sutter对此事的看法:http://herbsutter.com/2013/06/13/gotw-93-solution-auto-variables-part-2/ - rafak
显示剩余5条评论
15个回答

158

我认为在难以一眼确定类型的情况下,应该使用auto关键字,但表达式右侧的类型是显而易见的。例如,使用:

my_multi_type::nth_index<2>::type::key_type::composite_key_type::
    key_extractor_tuple::tail_type::head_type::result_type

要在boost::multi_index中获取复合键类型,即使您知道它是int,也不能只写int,因为它将来可能会更改。在这种情况下,我会写auto
因此,如果auto关键字在特定情况下提高了可读性,则使用它。当读者明确知道auto代表的类型时,可以写auto
以下是一些示例:
auto foo = std::make_shared<Foo>();   // obvious
auto foo = bla();                     // unclear. don't know which type `foo` has

const size_t max_size = 100;
for ( auto x = max_size; x > 0; --x ) // unclear. could lead to the errors
                                      // since max_size is unsigned

std::vector<some_class> v;
for ( auto it = v.begin(); it != v.end(); ++it )
                                      // ok, since I know that `it` has an iterator type
                                      // (don't really care which one in this context)

24
这似乎不是一个很好的建议。你有多少次不知道对象的类型?(除了模板之外)。如果你不知道类型,查一下,不要懒得使用自动功能。 - Paul Manta
13
@Paul: 通常情况下,你只需要知道或只需要知道类型的最重要的部分,比如说它是一个迭代器,但你并不知道也不关心它是向量的迭代器还是链表的迭代器;在这些情况下,你真的不希望花费时间和屏幕空间来弄清楚如何写出这些类型。 - Lie Ryan
54
不管C++的忠实支持者是否喜欢,C++0x都将吸引那些从未使用过C++的人,并且这些人将在许多地方使用auto关键字。 - Prof. Falken
8
@R.MartinhoFernandes - 不,唯一清楚的是无论 bla() 返回什么,你都将其传递给了 foo - Luis Machuca
9
@LuisMachuca,我的回复是一种嬉皮笑脸的方式,旨在表达通过给出一个糟糕的变量和函数命名的教科书范例,并将其可读性差归咎于类型推断,是不诚实的。 - R. Martinho Fernandes
显示剩余12条评论

65

尽可能使用 auto —— 特别是 const auto,这样副作用就不太需要担忧了。你不必担心类型问题,除非是显而易见的情况,但类型仍然会被静态验证,并且可以避免一些重复。在无法使用 auto 的情况下,可以使用 decltype 来表达语义上基于表达式的 约定 类型。你的代码看起来会有所不同,但这是积极的变化。


14
我会特别说一下 'const auto&'。 - Viktor Sehr
26
到处使用 auto 关键字会使你的代码难以阅读和调试,因为在阅读代码时你必须自己推断变量类型。请注意不要改变原意。 - SubMachine
5
@SubMachine:除非你使用记事本而不是一些合适的集成开发环境,否则你不需要自己推断类型。 - Young
8
@Young,我并不是说你不应该使用它,我是说你应该聪明地使用它——在你认为使用自动化可以增强代码可读性的情况下使用它,在它影响代码意图的情况下避免使用它。针对“适当”的集成开发环境(IDE)并不总是可用的,即使有时可以使用,悬停变量获取其类型也不如将类型作为代码的一部分直接显示方便。 - SubMachine
4
在正式的代码审查中,是否存在智能感知? - user997112
显示剩余4条评论

60

很简单。当你不关心类型是什么时,就使用它。例如

for (const auto & i : some_container) {
   ...

我在这里关心的只是i是容器中的任何内容。

这有点像typedefs。

typedef float Height;
typedef double Weight;
//....
Height h;
Weight w;

在这里,我不关心hw是浮点数还是双精度浮点数,只需要它们是适合表示身高和体重的任何类型。

或者考虑

for (auto i = some_container .begin (); ...

我关心的是一个适合的迭代器,支持operator++(),这在某种程度上类似于鸭子类型。

此外,lambda表达式的类型无法显式表示,因此使用auto f = []...是一种好的风格。另一种选择是将其转换为std::function,但会带来额外的开销。

我真的想不出什么“滥用”auto的例子。我能想到的最接近的是放弃对某些重要类型的显式转换 - 但你不会使用auto来实现,你会构造一个期望类型的对象。

反例(借用别人的回答):

auto i = SomeClass();
for (auto x = make_unsigned (y); ...)

在这里我们关心类型,因此我们应该写成Someclass i;for(unsigned x = y;...


1
如果您不关心类型,它应该为通用编程模板化。 - user997112
自 C++20 开始,auto 关键字可以用于函数模板中的参数类型推断。 - Anderson Green

49

C++ and Beyond 2012Ask Us Anything面板上,Andrei Alexandrescu、Scott Meyers和Herb Sutter之间有一个精彩的交流,谈论何时使用和不使用auto。跳到第25:03分钟,进行4分钟的讨论。三位演讲者都提出了应该记在心中的优秀观点,以便在何时使用auto

我强烈鼓励人们自己得出结论,但我的理解是要在任何地方都使用auto除非

  1. 它会影响可读性
  2. 存在对自动类型转换的担忧(例如从构造函数、赋值、模板中间类型、整数宽度之间的隐式转换)

大量使用explicit可以帮助减少对后者的担忧,从而减少前者成为问题的时间。

重新表述Herb所说的话,“如果你不在做X、Y和Z,就使用auto。了解X、Y和Z是什么,然后在其他地方都使用auto。”

4
可能还值得链接到“几乎总是自动”——http://herbsutter.com/2013/08/12/gotw-94-solution-aaa-style-almost-always-auto/ - Rob Starling
不错的推荐...很棒的交谈。Herb改变了我对“auto”使用的看法。 - kmiklas
这是cppCoreGuidelines中的建议(由Herb Sutter和Bjarne Stroustrup撰写):https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#es11-use-auto-to-avoid-redundant-repetition-of-type-names - kingsjester

45

尽情使用吧。在编写代码时,只要方便,就可以随处使用auto

任何语言中的新功能都会被一些类型的程序员过度使用。只有经验丰富的程序员(不是新手)通过适度的过度使用,才能使其他经验丰富的程序员学会正确使用的范围。极端的过度使用通常是不好的,但也可能是好的,因为这种过度使用可能会导致改进该功能或更好地替换它。

但如果我正在处理超过几行的代码,则会考虑根据需要显式指定类型,因为这样可以提高可读性和维护性。

auto foo = bla();

在类型不被明确指定的情况下,我可能会想要修改这些行以包含类型。第一个例子非常好,因为类型只被声明了一次,auto让我们避免了两次写杂乱的模板类型。太棒了C++++。但是,如果类型在附近的代码中没有很容易地显示出来,那么在C++及其直接的后继语言中,显示零次类型会让我感到紧张。对于其他设计用于更高级别抽象、多态性和通用性的语言,这样做则是可以接受的。


4
人们为什么总是用“长类型名称”来辩解“自动”(auto)关键字,而使用“using”语句可以用别名代替冗长的类型名称?例如:using alias = aVeryLongName。 - user997112
1
编写代码变得更容易了,但通常阅读代码变得更加困难。当代码变得越来越长,团队也变得越来越庞大,一个人负责编写一行代码,其他人则需要阅读它。因此,开发人员的工作变得越来越多地是阅读代码。 - kingsjester
为什么有些人试图辩解不使用auto,因为他们使用无用的变量和函数名称。实际变量类型通常比一个好名字提供的上下文含义不太重要。 - Matthew Whited

41

是的,过度使用它会影响可读性。我建议在以下情况下使用它:当确切类型很长、难以念出或者对可读性不重要,并且变量的生命周期短暂时。例如,迭代器类型通常很长并且不重要,因此可以使用 auto

   for(auto i = container.begin(); i != container.end(); ++i);

auto在这里不会影响可读性。

另一个例子是解析器规则类型,可能会变得冗长和复杂。比较一下:

   auto spaces = space & space & space;

使用

r_and_t<r_and_t<r_char_t<char>&, r_char_t<char>&>, r_char_t<char>&> spaces = 
   space & space & space;

另一方面,当类型已知且简单时,最好明确地声明它:

int i = foo();

与其说

auto i = foo();

2
当然,语言中有一个基于范围的for循环,使得你的第一个示例不那么令人兴奋。8v) - Fred Larson
4
如果您的边界不是“begin()”和“end()”,或者步长不是1,或者您在循环时修改了容器,那么范围for语句将无法帮助您。 - Dennis Zickefoose
6
@geotavros:r_and_t<r_and_t<r_char_t<char>&, r_char_t<char>&>, r_char_t<char>&> 是什么意思? - Dennis Zickefoose
7
或者你可以查看space的类型,并搜索相关信息。这是更有用的信息...毕竟,问题不是“这个新变量的类型是什么”,而是“space & space & space是什么意思?”表达式的实际类型只是噪音。 - Dennis Zickefoose
@DennisZickefoose 任何时候,如果您的边界不是begin()和end(),或者步长不是1,或者在循环中修改容器,您应该使用或制作一个算法。 - R. Martinho Fernandes
显示剩余5条评论

24

auto 在与表达式模板结合使用时可能非常危险,这在线性代数库(如Eigen或OpenCV)中经常使用。

auto A = Matrix(...);
auto B = Matrix(...);
auto C = A * B; // C is not a matrix. It is a matrix EXPRESSION.
cout << C; // The expression is evaluated and gives the expected result.
... // <code modifying A or B>
cout << C; // The expression is evaluated AGAIN and gives a DIFFERENT result.

由于这类型错误所导致的Bug非常难以调试。如果你一定要使用从左至右声明风格中的auto,那么一个可能的解决方法是显式地将结果转换为期望的类型。

auto C = Matrix(A * B); // The expression is now evaluated immediately.

2
这似乎是一种奇怪的行为。如果我正在对两个矩阵进行乘法运算,即使操作是惰性的,我也不希望它可以重新评估,我期望在初始评估之后它能保持已评估的状态。如果你想要改变参数而不修改原始参数,那么你最终还是需要重建表达式吗?或者它是为流处理类型的情况设计的,其中原始参数不断变化但过程保持不变? - JAB
2
无论A*B表达式是复制到auto变量还是其他地方,您所描述的行为仍然存在。 - xtofl
没有任何一个使用 auto 关键字的 bug 是由它引起的。 - Jim Balter
@JAB:它旨在支持操作之间的简化和协同作用,例如diag(A * B)不必浪费计算非对角线元素的周期。 - Ben Voigt
2
我还发现了像'for (auto o : vecContainer)'这样的代码,其中'o'是一个重量级对象,每次都会被复制。我的建议是将其用于它最初的目的,即模板(具有困难的推导指南)和费力的typedef。即使如此,您仍然必须小心并区分auto和auto&。 - gast128
任何曾经参与过真正的长期大规模项目的人都非常清楚你所说的,使用自动调试代码是地狱深渊一般的噩梦。 - user2645752

10

我不受限制地使用auto,也没有遇到任何问题。有时候我甚至会在简单类型(如int)中使用它。这使得C++对我来说更像是一种高级语言,并允许像Python一样声明变量。在编写Python代码后,我甚至有时会写例如:

auto i = MyClass();

而不是

MyClass i;

这是一个例子,我认为在这种情况下滥用auto关键字。

通常我并不关心对象的确切类型,而更关心它的功能。由于函数名称通常会描述它们返回的对象类型,因此auto并不会有什么问题:例如,在auto s = mycollection.size()中,我可以猜测s将是某种整数类型,而在少见的情况下,如果我需要了解确切的类型,我们可以检查函数原型(我的意思是,我更喜欢在需要信息时再进行检查,而不是在编写代码之前就提前检查一遍,以防万一会在某些时候派上用场,如int_type s = mycollection.size())。

关于来自已接受答案的示例:

for ( auto x = max_size; x > 0; --x )

在我的代码中,我仍然在这种情况下使用auto,如果我想让x是无符号的,那么我会使用一个实用函数,名为make_unsigned,它清楚地表达了我的顾虑:

for ( auto x = make_unsigned(max_size); x > 0; --x )

免责声明:我只是描述了我的使用情况,我没有资格给出建议!


1
@ChristianRau:并不是在讽刺。请注意,我没有建议使用auto i = MyClass() - rafak
3
后续:请参阅:http://herbsutter.com/2013/06/13/gotw-93-solution-auto-variables-part-2/。建议使用例如`as_unsigned`,甚至可以使用`auto w = widget{};`。 - rafak

3

C++程序的一个主要问题是它允许使用未初始化的变量,这会导致令人讨厌的不确定性程序行为。需要注意的是,现代编译器现在会抛出适当/消息警告信息,如果程序试图使用它。

只是为了说明这一点,考虑下面的C++程序:

int main() {
    int x;
    int y = 0;
    y += x;
}

如果我使用现代编译器(GCC)编译此程序,它会出现警告。如果我们处理真正复杂的生产代码,则此类警告可能不太明显。请注意,HTML标签将被保留。

main.cpp: In function 'int main()':

main.cpp:4:8: warning: 'x' is used uninitialized in this function [-Wuninitialized]

y += x;

    ^

================================================================================= 现在,如果我们更改使用auto的程序,然后编译,我们将得到以下结果:

int main() {
    auto x;
    auto y = 0;
    y += x;
}

main.cpp: In function 'int main()':

main.cpp:2:10: error: declaration of 'auto x' has no initializer

 auto x;

      ^
使用auto关键字,可以避免使用未初始化的变量。如果我们开始使用auto,那么这是一个重要的优势(免费获得)。C++专家Herb Shutter在他的CppCon14演讲中解释了这个概念和其他伟大的现代C++概念:

回到基础!现代C++风格的基本要素


2
是的。而且,你可以通过初始化为0i、0u、0l、0ul、0.0f、0.0或者int()、unsigned()、double()等来指定类型。 - Robinson
7
这个论点很荒谬。你说“你不能忘记初始化器,否则会得到编译器错误”,但这需要你记住使用auto - Lightness Races in Orbit
1
@LightnessRacesinOrbit:我从Herb Sutter的演讲中学到了这个概念。我认为这是来自Herb的合乎逻辑/实用的建议,因此想与社区分享。 - Mantosh Kumar
4
我认为这并不是使用 auto 的好动机。只需要打开编译器的未初始化变量警告标志并解决所有警告即可。这并不是说你不应该使用 auto - 但你不需要它来避免这个问题。 - einpoklum

2

我注意到的一个危险是参考文献方面。

例如:

MyBigObject& ref_to_big_object= big_object;
auto another_ref = ref_to_big_object; // ?

问题在于,这里的another_ref实际上不是一个引用,而是MyBigObject而不是MyBigObject&。你最终会复制一个大对象,却没有意识到这一点。
如果你直接从一个方法中获取引用,你可能不会想到它实际上是什么。
auto another_ref = function_returning_ref_to_big_object();

如果您需要,可以使用"auto&"或"const auto&"

MyBigObject& ref_to_big_object= big_object;
auto& another_ref = ref_to_big_object;
const auto& yet_another_ref = function_returning_ref_to_big_object();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接