自动会使C ++代码更难理解吗?


122

我看过Herb Sutter的一次会议,他鼓励每个C ++程序员使用auto

前段时间,我不得不阅读C#代码var,该代码被广泛使用,并且代码很难理解-每次var使用时,我都必须检查右侧的返回类型。有时不止一次,因为一段时间后我忘记了变量的类型!

我知道编译器知道类型,而不必编写它,但是我们应该为程序员而不是为编译器编写代码已广为接受。

我也知道这更容易编写:

auto x = GetX();

比:

someWeirdTemplate<someOtherVeryLongNameType, ...>::someOtherLongType x = GetX();

但这仅写入一次,并且GetX()多次检查了返回类型以了解其类型x

这让我感到奇怪-是否auto会使C ++代码更难理解?


29
您是否真的需要每次检查返回类型?为什么从代码中看不清类型?auto当它们已经很难阅读时,通常可能会使事情变得更难阅读,例如,函数太长,变量命名错误等。在具有恰当命名变量的短函数上,知道类型应该是#1简单或#2不相关的类型之一。
R. Martinho Fernandes

25
使用的“艺术” auto很像确定何时使用typedef。由您决定何时阻碍和何时提供帮助。
ahenderson

18
我以为我遇到了同样的问题,但是后来我意识到我可以不了解类型而只了解代码。例如:“自动idx = get_index();” 所以idx是保存索引的东西。在大多数情况下,确切的类型是无关紧要的。
PlasmaHH 2012年

31
因此,不要写auto x = GetX();,选择一个比x实际更好的名称,它可以实际告诉您它在特定上下文中的作用 ……不管怎么说,它通常比其类型更有用。
乔纳森·韦克利

11
如果使用更多的类型推断使程序员难以阅读代码,则代码或程序员都需要进行认真的改进。
CA McCann

Answers:


99

简短的回答:更全面地说,我目前的看法auto是,auto除非您明确希望进行转换,否则应默认使用。(更准确地说,“ ...除非您要显式地提交给类型,这几乎总是因为您要进行转换。”)

更长的答案和理由:

auto仅当您确实要显式提交类型时才写一个显式类型(而不是),这几乎总是意味着您要显式获得对该类型的转换。我想起了两个主要情况:

  • (通用)推导的initializer_list惊喜。如果您不想要,请说出类型-即,明确要求进行转换。auto x = { 1 };initializer_listinitializer_list
  • (罕见)表达式模板的大小写,例如,auto x = matrix1 * matrix 2 + matrix3;捕获了程序员不希望看到的助手或代理类型。在许多情况下,捕获该类型既好又无害,但是有时如果您真的希望它折叠并进行计算,请说出类型-即再次明确要求进行转换。

auto否则,默认情况下默认例行使用,因为使用auto避免了陷阱,并使您的代码更正确,更可维护,更健壮并且更有效。按照“首先写清楚和正确”的精神,按从大到小的顺序排列:

  • 正确性:使用auto保证,您将获得正确的类型。俗话说,如果你重复自己(重复说一遍),你就会并且会撒谎(弄错了)。这是一个通常的示例:void f( const vector<int>& v ) { for( /*…*-至此,如果您显式地编写迭代器的类型,则您要记住要写const_iterator(对吗?),而auto只是弄对了。
  • 可维护性和健壮性:使用auto可以使您的代码在发生更改时更加健壮,因为当表达式的类型更改时,auto将继续解析为正确的类型。如果改用显式类型,则当新类型转换为旧类型时,更改表达式的类型将注入静默转换,或者当新类型仍然有效时(如旧类型但不转换为旧类型),不必要的构建中断类型(例如,当您更改map到一个unordered_map,这始终是很好,如果你不是靠命令,使用auto您的迭代器,你会无缝地从切换map<>::iteratorunordered_map<>::iterator,但使用map<>::iterator 任何地方都明确表示您将浪费宝贵的时间在机械代码修复问题上,除非实习生走了过去,您可以对它们进行无聊的工作)。
  • 性能:由于auto保证不会发生任何隐式转换,因此默认情况下它保证了更好的性能。相反,如果您说类型,并且需要转换,那么无论您是否期望,您通常都会默默地获得转换。
  • 可用性:auto对于难于拼写和说不出来的类型(例如lambda和模板助手),使用唯一的好选择,它缺少求助于重复decltype表达式或效率低下的间接指令(如)std::function
  • 便利性:而且,是的,auto打字更少。我提到最后一个是为了保持完整性,因为这是喜欢它的常见原因,但这并不是使用它的最大理由。

因此:auto默认情况下,首选说。它提供了如此多的简单性,性能和清晰度优势,如果不这样做,只会伤害自己(以及代码的未来维护者)。仅在真正需要时才提交显式类型,这几乎总是意味着您想要显式转换。

是的,有(现在的)一个GotW这一点。


14
即使我确实想要转换,我也认为自动有用。它使我可以明确地要求进行转换,而无需重复以下类型:auto x = static_cast<X>(y)。这样static_cast可以清楚地表明转换是有意的,并且避免了编译器有关该转换的警告。通常情况下,避免编译器警告不是很好,但是我没有收到关于在编写时经过仔细考虑的转换的警告,这是可以的static_cast。尽管如果现在没有警告,我不会这样做,但是如果类型以潜在危险的方式更改,我希望将来获得警告。
Bjarke Hammersholt Roune

6
我发现的一件事auto是,我们应该努力针对接口(不是从OOP的角度)进行编程,而不是针对特定的实现进行编程。实际上,模板也是一样。您是否抱怨“难以阅读的代码”,因为您有在T各处使用的模板类型参数?不,我不这么认为。在模板中,我们也针对接口进行编码,很多人都称其为编译时鸭式输入。
Xeo 2012年

6
“使用自动保证您将获得正确的类型。” 完全不对。它仅保证您将获得代码其他部分规定的类型。将其隐藏在后面时,是否完全正确还不清楚auto
Lightness Races in Orbit

我真的感到惊讶的是,没有人关心IDE。即使是现代IDE也无法正确支持在出现auto变量的情况下跳转到类/结构定义,但是几乎所有的IDE都使用显式类型规范正确地做到了这一点。没有人使用IDE吗?每个人都只使用int / float / bool变量吗?每个人都喜欢使用库的外部文档,而不是自己记录的标头?
avtomaton

GotW:herbutterutter.com/2013/08/12/…我看不出“ initializer_list惊喜”是多么令人惊讶;=RHS的大括号在任何其他解释中都没有多大意义(大括号的init列表,但是您需要知道要初始化的内容,这与矛盾auto)。而其中,令人惊讶的是auto i{1}还演绎着initializer_list,尽管这意味着不 采取这种支撑初始化列表,而是借此表达和使用它的类型 ...但我们得到的initializer_list也有。幸运的是,C ++ 17很好地解决了所有这些问题。
underscore_d

112

这是个案情况。

它有时使代码更难理解,有时则难以理解。举个例子:

void foo(const std::map<int, std::string>& x)
{
   for ( auto it = x.begin() ; it != x.end() ; it++ )
   { 
       //....
   }
}

与实际的迭代器声明相比,它绝对易于理解,并且编写起来也更加容易。

我已经使用C ++一段时间了,但是我可以保证在此过程中第一枪const_iterator便会遇到编译器错误,因为我会忘记了,最初会选择iterator... :)

我会在这种情况下使用它,但实际上不会混淆类型(如您的情况),但这纯粹是主观的。


45
究竟。谁在乎这种类型。这是一个迭代器。我不在乎类型,我只需要知道可以使用它进行迭代即可。
R. Martinho Fernandes

5
+1。即使您确实命名了该类型,也将其命名为std::map<int, std::string>::const_iterator,因此该名称似乎并不能告诉您有关该类型的更多信息。
史蒂夫·杰索普

4
@SteveJessop:它至少告诉我两件事:关键是int,值是std::string。:)
Nawaz 2012年

16
@Nawaz:it->second由于它是const迭代器,因此您无法分配给它。所有这些信息都重复了上一行的内容const std::map<int, std::string>& x。多次说一句话有时会更好,但绝不是一般规则:-)
Steve Jessop 2012年

11
TBH我希望更清楚for (anX : x)一点,我们只是在迭代x。你需要一个迭代正常的情况是,当你正在修改的容器,但x就是const&
MSalters

94

换一种方式来看。你写:

std::cout << (foo() + bar()) << "\n";

要么:

// it is important to know the types of these values
int f = foo();
size_t b = bar();
size_t total = f + b;

std::cout << total << "\n";

有时,无助地明确拼写出该类型。

是否需要提及类型的决定与是否要通过定义中间变量在多个语句之间拆分代码的决定不同。在C ++ 03中,两者是链接在一起的,您可以考虑auto将它们分开。

有时使类型显式很有用:

// seems legit    
if (foo() < bar()) { ... }

// ah, there's something tricky going on here, a mixed comparison
if ((unsigned int)foo() < bar()) { ... }

在声明变量的情况下,使用auto可以像许多表达式一样直言不讳。您可能应该尝试自己决定什么时候可以提高可读性以及什么时候会降低可读性。

您可能会说混合有符号和无符号类型是一开始的错误(实际上,有些人甚至认为完全不应使用无符号类型)。可以说这是一个错误的原因是,由于行为不同,它使操作数的类型至关重要。如果需要了解值的类型是一件坏事,那么不必了解它们也不是一件坏事。因此,只要代码没有因为其他原因而引起混淆,那就可以了auto,对吧?;-)

特别编写通用代码时存在以下情况:一个变量的实际类型不应该是很重要的,重要的是它满足所需的接口。因此auto提供了一个抽象级别,您可以忽略该类型(但它知道编译器当然不会)。在适当的抽象级别上工作可以极大地提高可读性,而在“错误”级别上工作会使阅读代码成为徒劳。


21
+1 auto允许您创建具有无法命名或不感兴趣类型的命名变量。有意义的名称可能很有用。
Mankarse

如果将unsigned正确使用,则混合使用signed和unsigned:模块化算术。如果您将unsigned误用于正整数,则不是这样。几乎没有程序可以使用unsigned,但是核心语言会sizeof在您身上强制使用unsigned的inane定义。
curiousguy18年

27

IMO,您正在反向看待这个问题。

auto导致不可读甚至不那么可读的代码并不是问题。(希望)对返回值使用显式类型可以弥补以下事实:(显然)不清楚某个特定函数将返回哪种类型。

至少在我看来,如果您的函数的返回类型不是立即显而易见的,那就是您的问题所在。函数的功能应从其名称中显而易见,而返回值的类型应从其功能中显而易见。如果不是,那才是问题的真正根源。

如果这里有问题,那就不是auto。它与其余代码一起使用,并且显式类型很有可能只是一个创可贴,足以使您避免看到和/或解决核心问题。解决了实际问题后,auto通常使用代码的可读性就可以了。

公平地说,我想补充一点:我已经处理了一些情况,这些事情并没有您所希望的那么明显,并且解决该问题也相当站不住脚。仅举一个例子,我几年前曾为一家曾与另一家公司合并的公司做过一些咨询。他们最终得到的代码库比真正合并的“库在一起”更多。组成程序出于相似的目的开始使用不同的(但非常相似的)库,尽管它们正在努力更干净地合并事物,但它们仍然这样做。在很多情况下,猜测给定函数将返回哪种类型的唯一方法是知道该函数的起源。

即使在这种情况下,您也可以使很多事情变得更加清晰。在这种情况下,所有代码都始于全局名称空间。只需将相当数量的名称移入某些名称空间,就可以消除名称冲突并简化类型跟踪。


17

我不喜欢一般用途的汽车有几个原因:

  1. 您可以重构代码而无需修改它。是的,这是经常列举为使用auto的好处之一。只需更改函数的返回类型,如果调用该函数的所有代码都使用了auto,则无需额外的工作!您点击编译,它会生成-0个警告,0个错误-并且您只需继续检查代码即可,而无需处理检查和可能修改该函数使用的80个位置的麻烦。

但是,等等,这真的是一个好主意吗?如果在六个用例中类型很重要,而现在的代码实际上却表现不同,该怎么办?通过不仅修改输入值,而且修改调用该函数的其他类的私有实现的行为本身,这也可能隐式破坏封装。

1a。我相信“自我记录代码”的概念。自我记录代码背后的原因是注释趋于过时,不再反映代码在做什么,而代码本身(如果以显式方式编写)是不言自明的,始终保持最新状态它的意图,不会让您对陈旧的评论感到困惑。如果可以在无需修改代码本身的情况下更改类型,那么代码/变量本身将变得过时。例如:

自动bThreadOK = CheckThreadHealth();

除非问题是CheckThreadHealth()在某个时候被重构为返回一个指示错误状态的枚举值(如果有),而不是布尔值。但是进行更改的人错过了检查这行特定代码的过程,并且编译器没有帮助,因为编译时没有警告或错误。

  1. 您可能永远都不知道实际的类型是什么。这通常也被列为汽车的主要“好处”。当您只能说“谁在乎?它会编译!”时,为什么要了解功能为您提供的功能呢?

它甚至可能是一种作品。我说这种方法是可行的,因为即使您为每次循环迭代都复制了一个500字节的结构,因此您可以检查其上的单个值,但是代码仍然可以完全正常工作。因此,即使您的单元测试也无法帮助您意识到不良的代码正隐藏在这种简单且无辜的汽车后面。扫描文件的大多数其他人也不会乍一看。

如果您不知道类型是什么,也可能使情况变得更糟,但是您选择的变量名称对其含义进行了错误的假设,实际上实现了与1a中相同的结果,但是从一开始就而不是后重构。

  1. 最初编写代码时输入代码不是编程中最耗时的部分。是的,auto使最初编写某些代码的速度更快。作为免责声明,我确实输入> 100 WPM,所以它可能不会像其他人那样困扰我。但是,如果我只需要整天编写新代码,我将是一个快乐的露营者。编程中最耗时的部分是诊断代码中难以重现的边缘错误,这些错误通常是由细微的非显而易见的问题引起的,例如,可能会引入对auto的过度使用(引用与复制,有符号与无符号,浮点与整数,布尔与指针等)。

在我看来,显而易见,auto最初是作为标准库模板类型的可怕语法的变通方法而引入的。而不是尝试修复人们已经熟悉的模板语法-由于可能会破坏所有现有代码,因此这几乎也是不可能的-添加一个基本上可以隐藏问题的关键字。本质上,您可能称之为“黑客”。

实际上,我对标准库容器中的auto使用没有异议。显然是为关键字创建的,标准库中的函数不太可能从根本上改变目的(或类型),从而使auto相对安全地使用。但是对于在您自己的代码和接口中使用它可能会更加不稳定,并且可能会进行更根本的更改,我将非常谨慎。

auto的另一个有用的应用程序可以增强语言的功能,即在与类型无关的宏中创建临时变量。这是您以前真正无法做到的事情,但是您现在就可以做到。


4
你钉了 希望我能给这个+2。
cmaster

一个很好的“该死的谨慎”答案。@cmaster:在那里。
Deduplicator 2015年

我发现了一个更有用的案例:auto something = std::make_shared<TypeWithLongName<SomeParam>>(a,b,c);。:-)
Notinlist,2015年

14

是的,如果不使用,可以更轻松地知道变量的类型auto。问题是:您是否需要知道变量的类型才能阅读代码?有时答案是肯定的,有时则是。例如,从中获取迭代器时std::vector<int>,您是否需要知道它是std::vector<int>::iterator或就auto iterator = ...;足够了?任何人都想使用迭代器的事情都由它是一个迭代器的事实给出-只是具体的类型无关紧要。

使用auto在这些情况下,当它不会使你的代码难以阅读。


12

就个人而言,我auto仅在程序员绝对清楚它是什么的情况下使用。

例子1

std::map <KeyClass, ValueClass> m;
// ...
auto I = m.find (something); // OK, find returns an iterator, everyone knows that

例子2

MyClass myObj;
auto ret = myObj.FindRecord (something)// NOT OK, everyone needs to go and check what FindRecord returns

5
这是不良命名会损害可读性的一个明显例子,并非真正意义上的自动。没有人知道“ DoSomethingWeird”的作用是什么,因此无论是否使用auto都不会使其更具可读性。您将不得不以任何一种方式检查文档。
R. Martinho Fernandes

4
好吧,现在好点了。不过,我仍然发现该变量的命名不正确,但仍然很麻烦。如果您写的auto record = myObj.FindRecord(something)话,很明显变量类型是记录的。命名it或类似名称可以使其清楚地返回迭代器。请注意,即使您没有使用auto,正确命名变量也意味着您无需跳回到声明即可从函数中的任何位置查看类型。我删除了我的反对意见,因为该示例现在还不是一个完整的稻草人,但在这里我仍然不赞成这样做。
R. Martinho Fernandes

2
要添加到@ R.MartinhoFernandes:问题是,“记录”到底是什么真的重要吗?我认为更重要的是,它是一条记录,实际的基础原始类型是另一个抽象层。.因此,如果没有auto,可能会有:MyClass::RecordTy record = myObj.FindRecord (something)
paul23 2012年

2
@ paul23:如果您唯一的反对意见是“我不知道如何使用此功能”,那么使用自动与类型进行交互会为您带来好处。无论哪种方式都会使您查找它。
GManNickG

3
@GManNickG告诉我不重要的确切类型。
paul23,2012年

10

这个问题征求意见,不同的程序员对此有所不同,但我会拒绝。实际上,在许多情况下恰好相反,auto通过允许程序员专注于逻辑而不是细节,可以使代码更易于理解。

面对复杂的模板类型,尤其如此。这是一个简化的人为示例。哪个更容易理解?

for( std::map<std::pair<Foo,Bar>, std::pair<Baz, Bot>, std::less<BazBot>>::const_iterator it = things_.begin(); it != things_.end(); ++it )

.. 要么...

for( auto it = things_.begin(); it != things_.end(); ++it )

有些人会说第二个更容易理解,其他人可能会说第一个。还有一些人可能会说,随意使用auto可能会导致使用它的程序员愚蠢,但这是另一回事。


4
+1哈哈,每个人都在演示std::map示例,此外还带有复杂的模板参数。
纳瓦兹2012年

1
@Nawaz:使用maps 可以很容易地得出疯狂的冗长的模板名称。:)
John Dibling 2012年

@Nawaz:但是我不知道为什么没有人会提出基于范围的for循环作为更好和更易读的替代方法……
PlasmaHH 2012年

1
@PlasmaHH,并非所有带有迭代器的循环都可以用基于范围的替换,for例如,如果迭代器在循环体内无效,因此需要预先增加或根本不增加。
乔纳森·韦克利

@PlasmaHH:就我而言,MSVC10不执行基于范围的for循环。由于MSVC10是我的首选C ++ 11测试平台,因此我对它们并没有太多的经验。
John Dibling 2012年

8

到目前为止,有很多很好的答案,但是我想把重点放在最初的问题上,赫伯在他的建议中走得太远,不能auto自由使用。您的示例是一种使用auto明显损害可读性的情况。有人坚持认为,对于现代IDE来说,这不是问题,您可以将其悬停在变量上并查看类型,但我不同意:即使是始终使用IDE的人有时也需要单独查看代码段(认为代码回顾)。 (例如),而IDE则无济于事。

底线:auto在有帮助时使用:即for循环中的迭代器。当它使读者难以找出类型时,请不要使用它。


6

我很惊讶,没有人指出,如果没有明确的类型,auto会有所帮助。在这种情况下,您可以通过在模板中使用#define或typedef来查找实际的可用类型(有时并不容易)来解决此问题,或者仅使用auto。

假设您有一个函数,该函数返回平台特定类型的内容:

#ifdef PLATFROM1
__int256 getStuff();
#else //PLATFORM2
__int128 getStuff();
#endif

您希望使用女巫吗?

#ifdef PLATFORM1
__int256 stuff = getStuff();
#else
__int128 stuff = getStuff();
#endif

或者只是

auto stuff = getStuff();

当然可以

#define StuffType (...)

以及某处,但确实

StuffType stuff = getStuff();

真的告诉我有关x类型的更多信息吗?它告诉它是从那里返回的内容,但恰恰是auto是什么。这只是多余的-在这里“ stuff”被写了3次-我认为这比“ auto”版本的可读性差。


5
处理平台特定类型的正确方法是使用typedef它们。
cmaster

3

可读性是主观的;您需要查看情况并决定最好的方法。

正如您所指出的那样,如果不使用auto,则长声明会产生很多混乱。但是,正如您还指出的那样,简短声明可以删除可能有价值的类型信息。

最重要的是,我还要添加以下内容:确保您正在查看的是可读性而不是可写性。易于编写的代码通常不容易阅读,反之亦然。例如,如果我正在写,我会更喜欢自动。如果我正在阅读,也许是更长的声明。

然后就是一致性;这对您有多重要?您是要在某些部分使用auto还是在其他部分使用显式声明,还是在整个过程中使用一种一致的方法?


2

我将以可读性较低的代码为优势,并鼓励程序员越来越多地使用它。为什么?显然,如果使用auto的代码难以阅读,那么也将很难编写。程序员被迫使用有意义的变量名,以使其工作更好。
也许一开始程序员可能不会编写有意义的变量名。但是最终在修复错误或代码审查时,当他/她不得不向他人解释代码时,或者在不久的将来,他/她向维护人员解释代码时,程序员将意识到错误并会使用将来有意义的变量名。


2
充其量,人们会写一些变量名myComplexDerivedType来弥补丢失的类型,这会因类型重复(使用变量的所有位置)而使代码混乱,并诱使人们忽略变量名的目的。 。我的经验是,没有什么比在代码中积极地设置障碍没有效率更高了。
cmaster

2

我有两个准则:

  • 如果变量的类型很明显,那么编写起来很麻烦或者很难确定是否使用auto。

    auto range = 10.0f; // Obvious
    
    for (auto i = collection.cbegin(); i != cbegin(); ++i) // Tedious if collection type
    // is really long
    
    template <typename T> ... T t; auto result = t.get(); // Hard to determine as get()
    // might return various stuff
  • 如果需要特定的转换,或者结果类型不明显,可能会造成混淆。

    class B : A {}; A* foo = new B(); // 'Convert'
    
    class Factory { public: int foo(); float bar(); }; int f = foo(); // Not obvious

0

是。它降低了冗长性,但是常见的误解是冗长性降低了可读性。仅当您认为可读性是美的,而不是您理解代码的实际能力时才是正确的-使用auto并不能提高代码的理解能力。在最常引用的示例矢量迭代器中,使用auto可以提高代码的可读性。另一方面,您并不总是知道auto关键字会为您带来什么。您必须遵循与编译器相同的逻辑路径进行内部重构,并且在很多时候,尤其是对于迭代器,您将做出错误的假设。

最终,“自动”牺牲了代码的可读性和清晰度,以实现句法和美学上的“整洁”(这仅是必要的,因为迭代器具有不必要的复杂语法),并且可以在任何给定的行上少输入10个字符。这样做不值得冒险,也不值得付出长期的努力。

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.