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

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

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

我也知道更容易编写:

auto x = GetX();


比:

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


但这只编写了一次,并且多次检查了GetX()返回类型以了解x的类型。

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

评论

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

使用auto的“技巧”很像确定何时使用typedef。您可以自行决定何时阻碍和何时提供帮助。

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

因此,不要编写auto x = GetX();,而是选择一个比x更好的名称,该名称实际上会告诉您在特定上下文中它的功能……反正通常比其类型有用。

如果使用更多的类型推断使程序员难以阅读代码,则无论是代码还是程序员都需要进行认真的改进。

#1 楼

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

更长的答案和原理:

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


(常见)initializer_list引起auto x = { 1 };推导initializer_list的惊奇。如果您不希望使用initializer_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<>::iterator无缝切换到unordered_map<>::iterator,但是在所有地方都使用map<>::iterator明确意味着您'除非浪费实习生,否则您会浪费宝贵的时间,除非实习生走过,您可以对它们进行无聊的工作。)

性能:因为auto保证不会发生隐式转换,默认情况下,它保证更好的性能。相反,如果您说类型并且需要转换,那么无论您是否期望,您都会无声地获得转换。

可用性:使用auto是难于拼写的唯一好选择以及不确定的类型(例如lambda和模板帮助程序),没有采用重复的decltype表达式或效率较低的间接调用(如std::function)。

方便:是的,auto的键入较少。我之所以提到最后一个是出于完整性,是因为它是喜欢它的常见原因,但这并不是使用它的最大原因。
因此:默认情况下,最好说auto。它提供了如此多的简单性,性能和清晰度优势,如果不这样做,只会伤害自己(以及代码的未来维护者)。仅当您真正想要一个显式类型时才提交它,这几乎总是意味着您想要一个显式转换。

是的,关于(现在)有一个关于此的GotW。

评论


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

–巴克·哈默斯霍尔特·鲁恩
2012年12月26日在18:05

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

– Xeo
2012-12-27 13:09:

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

–轨道轻度竞赛
15年3月10日在19:06

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

–avtomaton
17-1-26在1:15



GotW:herbutterutter.com/2013/08/12/…我看不出“ initializer_list惊喜”是多么令人惊讶;大括号= RHS在任何其他解释中都没有多大意义(大括号的init列表,但是您需要知道要初始化的内容,这是auto的矛盾之处)。令人惊讶的是,自动i {1}还会推导出initializer_list,尽管它暗示不采用此括号的init-list而是采用此表达式并使用其类型...但我们也在那里获得initializer_list。幸运的是,C ++ 17很好地解决了所有这些问题。

– underscore_d
17年12月9日15:43

#2 楼

这是一个个案的情况。

有时会使代码难以理解,有时却很难。举个例子:

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


绝对比实际的迭代器声明更易于理解和编写。

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

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

评论


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

– R. Martinho Fernandes
2012-12-20 14:05

+1。即使您确实命名了类型,也要将其命名为std :: map :: const_iterator,所以无论如何名称都不能告诉您有关类型的更多信息。

–史蒂夫·杰索普(Steve Jessop)
2012-12-20 14:25

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

–纳瓦兹
2012-12-20 14:27

@Nawaz:而且您不能分配给它-> second,因为它是一个常量迭代器。所有这些信息都是上一行const std :: map &x的重复。多说几句有时会更好,但绝不是一般规则:-)

–史蒂夫·杰索普(Steve Jessop)
2012-12-20 14:31

TBH我更希望(anX:x)变得更明显,我们只是迭代x。需要迭代器的通常情况是在修改容器时,但是x是const&

–MSalters
2012年12月20日17:05

#3 楼

换一种方式来看。您是否写:

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()) { ... }


vs.

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


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

您可以辩解说,混合有符号和无符号类型是一个错误的开始(实际上,有人进一步争论说,不应完全使用无符号类型)。可以说这是一个错误的原因是,由于行为不同,它使得操作数的类型至关重要。如果需要了解值的类型是一件坏事,那么不必了解它们也不是一件坏事。因此,只要代码没有因为其他原因而令人困惑,这会使auto正常,对吗? ;-)

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

评论


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

– Mankarse
2012-12-20 14:27

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

–好奇
18年6月18日在11:19

#4 楼

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

auto导致代码不可读甚至可读性不高。 (希望)为返回值使用显式类型可以弥补以下事实:(显然)不清楚某个特定函数将返回哪种类型。

至少我认为,如果您的函数的返回类型不是立即显而易见的,那就是您的问题所在。该函数的作用从其名称应显而易见,而返回值的类型应从其作用显而易见。如果不是,那是问题的真正根源。

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

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

即使在这种情况下,您也可以帮助使几件事更清楚了。在这种情况下,所有代码都始于全局名称空间。只需将大量迁移到某些名称空间中,就可以消除名称冲突,并大大简化了类型跟踪。

评论


不同意。一个函数可以被惊人地命名,但是仍然不能告诉您正在使用哪个EXACT类。

–user997112
20-09-26在1:18

#5 楼

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


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

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

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

auto bThreadOK = CheckThreadHealth();

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


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

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

如果您不知道类型是什么,但是选择一个变量名可以使这种情况变得更糟。关于它是什么的错误假设,实际上实现了与1a中相同的结果,但是从一开始就实现了,而不是后重构。


在最初编写代码时键入代码是不正确的。是编程中最耗时的部分。是的,auto使最初编写某些代码的速度更快。作为免责声明,我确实输入> 100 WPM,所以也许它不会像其他人那样困扰我。但是,如果我只需要整天编写新代码,那我将是一个快乐的露营者。程序设计中最耗时的部分是诊断代码中难以重现的边缘错误,这些错误通常是由细微的非显而易见的问题引起的,例如,可能会引入对auto的过度使用(引用与复制,对我来说,很明显,引入auto最初是作为使用标准库模板类型的可怕语法的变通方法,这对我来说似乎很明显。而不是尝试修复人们已经熟悉的模板语法-由于可能会破坏所有现有代码,这也几乎是不可能的-添加一个基本上可以隐藏问题的关键字。本质上,您可能称之为“黑客”。

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

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

评论


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

–cmaster-恢复莫妮卡
15年7月25日在7:03

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

–重复数据删除器
15年7月25日在9:17

我发现了另一种有用的情况:auto something = std :: make_shared >(a,b,c);。 :-)

–通知清单
2015年11月17日14:53

我主要受到上述#3的困扰。您尝试阅读代码来查找错误,并且看到10行带有自动...没有任何线索表明下面的类型或它们的作用

– dev_null
19-10-24在11:04

#6 楼

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

在那些情况下使用auto iterator = ...;您的代码更难阅读。

评论


如果您不知道迭代器的类型,您如何知道是否有一个向量(通过* iter访问该元素)或一个映射(该元素是iter-> second)?

–user997112
20/09/26'1:21

#7 楼

我个人仅在对程序员绝对清楚的情况下使用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


评论


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

– R. Martinho Fernandes
2012-12-20 14:10

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

– R. Martinho Fernandes
2012-12-20 14:19

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

–paul23
2012-12-20 14:28

@ paul23:如果您唯一的反对意见是“我不知道如何使用此功能”,那么使用auto和type会使您受益。无论哪种方式都会使您查找它。

– GManNickG
2012年12月20日19:31

@GManNickG告诉我不重要的确切类型。

–paul23
2012年12月20日19:54

#8 楼

这个问题征求意见,不同的程序员对此有所不同,但我会拒绝。实际上,在许多情况下恰恰相反,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可能会导致使用它的程序员愚蠢,但这是另一回事。

评论


+1哈哈,每个人都在介绍std :: map示例,此外还带有复杂的模板参数。

–纳瓦兹
2012年12月20日下午14:15

@Nawaz:使用地图很容易得出疯狂的冗长的模板名称。 :)

– John Dibling
2012-12-20 14:17

@Nawaz:但是我不知道为什么没有人会提供基于范围的for循环作为更好,更易读的替代方案...

– PlasmaHH
2012-12-20 14:41

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

–乔纳森·韦克利
2012-12-20 14:43

@JonathanWakely:并非所有循环都使用映射迭代器,这些迭代器在读取时有点不直观,例如向量索引的简单循环,在这种情况下,使用auto不会带来更多可读性。

– PlasmaHH
2012-12-20 14:45

#9 楼

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

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

#10 楼

我很惊讶,没有人指出,如果没有明确的类型,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”版本的可读性更差。

评论


处理平台特定类型的正确方法是对它们进行typedef。

–cmaster-恢复莫妮卡
15年7月25日在6:42

@ cmaster-reinstatemonica使用using语句,而不是typedef。

–user997112
20/09/26'1:23

@ user997112 cppreference.com说:“类型别名声明和typedef声明之间没有区别。”因此,只要您为平台特定的类型提供适当的可移植名称,我都不关心您使用typedef还是使用声明类型别名。您的样式指南可能会声明对特定项目应使用using或typedef,但是一旦此类规则离开该范围,就等于或多或少地遵循了宗教性规则。而且我认为宗教规则在程序设计中是有害的。

–cmaster-恢复莫妮卡
20 Sep 26'7:13

因此,您认为他们添加了一个没有用的新关键字吗?

–user997112
20/09/26 '17:09

#11 楼

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

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

此外,我还要补充一点:请确保您正在查看的是可读性而不是可写性。易于编写的代码通常不容易阅读,反之亦然。例如,如果我正在写,我会更喜欢自动。如果我正在阅读,也许是更长的声明。

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

评论


“长声明会产生很多混乱”,一个“使用”语句消除了混乱。无需汽车。

–user997112
20/09/26'1:23

#12 楼

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

评论


充其量,您会得到人们写的变量名,例如myComplexDerivedType,以弥补缺失的类型,这会因类型重复(使用变量的所有位置)而使代码混乱,并诱使人们在其变量中忽略变量的用途名称。我的经验是,没有什么比在代码中积极设置障碍更没有生产力了。

–cmaster-恢复莫妮卡
15年7月25日在6:48

#13 楼

我有两个准则:



如果变量的类型很明显,那么编写起来会很乏味或很难
确定自动使用。

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




评论


您从使用auto而不是float获得了什么? 1个字符?然后您甚至还没有得到,因为您在末尾添加了“ f”。那有什么意义呢?

–user997112
20/09/26'1:25

#14 楼

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

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