【问题标题】:Why do I need to explicitly write the 'auto' keyword?为什么我需要显式编写'auto'关键字?
【发布时间】:2018-11-02 03:19:08
【问题描述】:

我正在从 C++98 转向 C++11,并且已经熟悉了 auto 关键字。我想知道如果编译器能够自动推断类型,为什么我们需要显式声明auto。我知道 C++ 是一种强类型语言,这是一个规则,但是如果不显式声明变量 auto 就不可能达到相同的结果吗?

【问题讨论】:

  • 记住 C 系列是区分大小写的。去调试一些 JS 代码,其中作者省略了“var”,并使用名称为“Bob”、“bob”和“boB”的单独变量。呃。
  • 即使有可能,这也是一个相当糟糕的主意。 Arguably the biggest weakness of Python(和类似的语言)是缺少声明语法。简单赋值会创建新变量是错误的主要来源。
  • @KonradRudolph:虽然 JS 的声明语法并没有更好。我认为他们的意思是无法以细粒度的方式限制变量的范围。
  • @Mehrdad “改变语义”≠“强制”。问题是 JavaScript 确实 接受隐式声明。是的,它们在语义上是不同的,但这丝毫没有帮助。
  • 另见双重问题“为什么真正的 Perl 用户使用“my”关键字stackoverflow.com/questions/8023959/why-use-strict-and-warnings/…

标签: c++ c++11 auto


【解决方案1】:

删除显式 auto 会破坏语言:

例如

int main()
{
    int n;
    {
        auto n = 0; // this shadows the outer n.
    }
}

您可以看到删除auto 不会遮蔽外部n

【讨论】:

  • 正在输入完全相同的内容。将分配与初始化区分开需要标准方面的任意选择。既然我们已经有了“任何可能是声明的东西就是声明”的规则,我们就踏入了非常混浊的水中。
  • 这不是问题。像 golang 一样,您显然可以使用 n := 0 之类的东西来引入新变量。为什么使用auto 是一个基于意见的问题。
  • @liliscent - 它是基于意见的吗? (a) 它已经是一个保留关键字。 (b) 意思很清楚。 (c) 它避免了引入新令牌的需要(如:=)。 (d) 它已经符合语法。我认为这里没有多少意见的余地。
  • @StoryTeller 如果x = f() 声明一个新变量(如果尚不存在),获取 f`s 返回值的类型,则不需要新标记... 需要 auto 显式声明一个然而,variable 降低了意外声明新变量的风险(例如,由于拼写错误......)。
  • @Aconcagua - “声明一个新变量(如果还不存在的话)” 但是阴影语言的一部分,并且仍然必须工作,就像拔示巴说明的那样。这是一个比人们想象的更大的问题。这不是从头开始设计语言,而是要改变一种活生生的语言。做起来难很多。有点像在超速行驶的汽车上更换车轮。
【解决方案2】:

您的问题有两种解释:

  • 为什么我们需要“自动”?我们不能直接放弃吗?
  • 为什么我们必须使用自动?如果没有给出,我们不能让它隐含吗?

Bathsheba answered 第一个解释很好,第二个解释如下(假设目前不存在其他声明;假设有效的 C++):

int f();
double g();

n = f(); // declares a new variable, type is int;
d = g(); // another new variable, type is double

if(n == d)
{
    n = 7; // reassigns n
    auto d = 2.0; // new d, shadowing the outer one
}

是可能的,其他语言可以很好地解决(好吧,除了阴影问题之外)......但在 C++ 中并非如此,问题(在第二种解释的意义)现在是:为什么?

这一次,答案不像第一个解释那样明显。不过有一点很明显:对关键字的明确要求使语言更安全(我不知道这是否是促使语言委员会做出决定的原因,但它仍然是一个重点):

grummel = f();

// ...

if(true)
{
    brummel = f();
  //^ uh, oh, a typo...
}

我们可以就此达成一致,不需要任何进一步的解释吗?

不要求 auto 更大的危险,[然而],它意味着在远离函数的地方(例如在头文件中)添加一个全局变量可能会改变原本的声明将该函数中的局部范围变量转换为对全局变量的赋值...具有潜在的灾难性(当然也非常令人困惑)的后果。

(引用 psmears' 评论,因为它的重要性 - 感谢您的提示)

【讨论】:

  • 在我看来,不需要auto 的更大危险在于,这意味着在远离函数的地方(例如在头文件中)添加全局变量可以改变什么旨在将该函数中的局部范围变量声明为对全局变量的赋值......具有潜在的灾难性(当然非常混乱)的后果。
  • @psmears 像 Python 这样的语言通过要求将变量明确指定为全局/非局部变量来避免这种情况;默认情况下,它只是创建一个具有该名称的新局部变量。 (当然,您可以从全局变量中读取而不需要 global <variable> 语句。)当然,这需要对 C++ 语言进行更多修改,因此可能不可行。
  • @JAB - 是的,我知道......我没有提到它,因为正如你所说,它需要对语言进行更多修改:)
  • FWIW,推动语言委员会的最有可能是历史。 AFAIK,最初编写 C 时,局部变量保存在堆栈中,C 要求首先在块中显式声明所有变量。这允许编译器在编译其余代码之前确定该块的存储要求,并允许它发出正确的指令序列以在堆栈上分配空间。 IIRC MOV R6 R5 SUB #nnn R6 在 PDP-11 上假设 R5 用作帧指针,R6 是堆栈指针。 nnn 是所需的存储字节数。
  • 人们确实设法使用 Python,每当新名称出现在赋值左侧时(即使该名称是拼写错误),它都会愉快地声明一个变量。但我确实认为这是该语言更严重的缺陷之一。
【解决方案3】:

如果不明确声明变量auto,就不可能达到相同的结果吗?

我将稍微改一下你的问题,以帮助你理解为什么需要auto

如果不显式使用类型占位符,是否不可能实现相同的结果?

这不是可能吗?当然这是“可能的”。问题是这样做是否值得。

其他语言中没有类型名的大多数语法以两种方式之一工作。有类似 Go 的方式,name := value; 声明一个变量。还有一种类似 Python 的方式,如果 name 之前没有声明过,name = value; 声明一个新变量。

让我们假设在 C++ 中应用这两种语法都没有语法问题(尽管我已经看到 identifier 后跟 : 在 C++ 中的意思是“制作标签”)。那么,与占位符相比,你失去了什么?

好吧,我不能再这样做了:

auto &name = get<0>(some_tuple);

看,auto 总是意味着“价值”。如果您想获得参考,您需要明确使用&amp;。如果赋值表达式是纯右值,它将正确地无法编译。这两种基于赋值的语法都无法区分引用和值。

现在,如果给定值是引用,您可以使此类赋值语法推断引用。但这意味着你不能这样做:

auto name = get<0>(some_tuple);

复制元组,创建一个独立于some_tuple的对象。有时,这正是你想要的。如果您想从带有auto name = get&lt;0&gt;(std::move(some_tuple)); 的元组中移动,这将更加有用。

好的,所以也许我们可以稍微扩展一下这些语法来解释这种区别。也许&amp;name := value;&amp;name = value; 意味着推断出像auto&amp; 这样的引用。

好的,好的。这个呢:

decltype(auto) name = some_thing();

哦,没错; C++ 实际上有two placeholders: auto and decltype(auto)。这个推论的基本思想是它的工作原理就像你做了decltype(expr) name = expr;一样。所以在我们的例子中,如果some_thing() 是一个对象,它就会推导出一个对象。如果some_thing()是一个引用,它会推导出一个引用。

当您在模板代码中工作并且不确定函数的返回值究竟是什么时,这非常有用。这对于转发来说非常有用,它是一个必不可少的工具,即使它没有被广泛使用。

所以现在我们需要在语法中添加更多内容。 name ::= value; 的意思是“做decltype(auto) 所做的事情”。我没有 Pythonic 变体的等价物。

看看这个语法,是不是很容易意外输入错误?不仅如此,它几乎不是自我记录的。即使您以前从未见过decltype(auto),它也足够大且足够明显,您至少可以很容易地看出发生了一些特别的事情。而::=:= 之间的视觉差异很小。

但那是意见的东西;还有更多实质性问题。看,所有这些都是基于使用赋值语法。嗯...那些你不能使用赋值语法的地方呢?像这样:

for(auto &x : container)

我们是否将其更改为for(&amp;x := container)?因为这似乎与基于范围的for非常不同的东西。看起来它是来自常规 for 循环的初始化语句,而不是基于范围的 for。它也将是与非推导案例不同的语法。

此外,复制初始化(使用 =)在 C++ 中与直接初始化(使用构造函数语法)不同。所以name := value;auto name(value) 可以使用的情况下可能不起作用。

当然,您可以声明 := 将使用直接初始化,但这与 C++ 其余部分的行为方式完全不一致。

此外,还有一件事:C++14。它为我们提供了一个有用的推导功能:返回类型推导。但这是基于占位符的。与基于范围的for 非常相似,它基本上基于编译器填充的类型名,而不是应用于特定名称和表达式的某些语法。

你看,所有这些问题都来自同一个来源:你正在发明全新的语法来声明变量。基于占位符的声明不必发明新的语法。他们使用与以前完全相同的语法;他们只是使用了一个新的关键字,它的作用类似于一种类型,但具有特殊的含义。这就是允许它在基于范围的for 和返回类型推导中工作的原因。它允许它有多种形式(auto vs. decltype(auto))。以此类推。

占位符之所以有效,是因为它们是解决问题的最简单方法,同时保留了使用实际类型名称的所有好处和通用性。如果您想出另一种与占位符一样通用的替代方案,那么它就不太可能像占位符那样简单。

除非它只是用不同的关键字或符号拼写占位符...

【讨论】:

  • 恕我直言,这是解决占位符选择背后的一些实质性理由的唯一答案。泛型 lambda 中的类型推导可能是另一个例子。很遗憾,这个答案仅仅因为它发布的有点晚而得到了这么少的支持......
  • @liliscent: "泛型 lambda 中的类型推导可能是另一个例子。" 我没有提到这一点,因为它与 auto 声明/返回值推导的语义不同.
  • @liliscent:确实,这个答案迟到了。上升。 (不过,一个改进是提到了 C++ 的想法,即如果某物可能是一个声明,那么它就是一个声明。)
【解决方案4】:

简而言之:auto 在某些情况下可能会被删除,但这会导致不一致。

首先,正如所指出的,C++ 中的声明语法是&lt;type&gt; &lt;varname&gt;。显式声明需要某种类型或至少有一个声明关键字。所以我们可以使用var &lt;varname&gt;declare &lt;varname&gt; 之类的,但auto 是C++ 中长期存在的关键字,是自动类型推断关键字的理想候选。

是否可以通过赋值隐式声明变量而不破坏一切?

有时是的。您不能在函数外部执行赋值,因此您可以在此处使用赋值语法进行声明。但是这样的做法会带来语言的不一致,可能会导致人为错误。

a = 0; // Error. Could be parsed as auto declaration instead.
int main() {
  return 0;
}

当涉及到任何类型的局部变量时,显式声明是它们控制变量范围的方式。

a = 1; // use a variable declared before or outside
auto b = 2; // declare a variable here

如果允许使用模棱两可的语法,声明全局变量可能会突然将局部隐式声明转换为赋值。查找这些转化需要检查所有内容。并且为了避免冲突,您需要为所有全局变量提供唯一名称,这会破坏作用域的整个想法。所以真的很糟糕。

【讨论】:

    【解决方案5】:

    auto 是一个关键字,您可以在通常需要指定类型的地方使用它。

      int x = some_function();
    

    可以通过自动推导出int 类型来使其更通用:

      auto x = some_function();
    

    所以这是对语言的保守扩展;它适合现有的语法。没有它x = some_function() 就变成了一个赋值语句,不再是一个声明。

    【讨论】:

      【解决方案6】:

      语法必须明确且向后兼容。

      如果 auto 被删除,将无法区分语句和定义。

      auto n = 0; // fine
      n=0; // statememt, n is undefined.
      

      【讨论】:

      • 重要的一点是auto 已经是一个关键字(但具有过时的含义),因此它不会破坏使用它作为名称的代码。这就是为什么没有选择更好的关键字(例如 varlet)的原因。
      • @Frax IMO auto 实际上是一个非常出色的关键字:它准确地表达了它的含义,即它用“自动类型”替换了类型名称。使用varlet 之类的关键字,您应该因此需要关键字即使 明确指定类型,即var int n = 0 或类似var n:Int = 0 的东西。这基本上是在 Rust 中完成的。
      • @leftaroundabout 虽然auto 在现有语法的上下文中绝对是优秀的,但我会说像var int x = 42 这样的基本变量定义,var x = 42int x = 42 作为简写,会如果考虑到历史内容,则比当前语法更有意义。但这主要是口味问题。但是,你是对的,我应该在我原来的评论中写“一个原因”而不是“一个原因”:)
      • @leftaroundabout: "auto 实际上是一个非常出色的关键字:它准确地表达了它的含义,即,它用'自动类型'替换了类型名称" 这不是真的。没有“自动型”。
      • @LightnessRacesinOrbit 在您可以使用auto 的任何给定上下文中,有一个自动类型(不同的类型,取决于表达式)。
      【解决方案7】:

      除了之前的答案,还有一个老屁的额外说明:看起来您可能认为能够在不以任何方式声明的情况下开始使用新变量是一种优势。

      在可能隐式定义变量的语言中,这可能是一个大问题,尤其是在较大的系统中。你打了一个错字,调试了几个小时才发现你无意中引入了一个值为零(或更糟)的变量 - blue vs bleulabel vs lable ...结果是你如果不彻底检查精确的变量名,就无法真正信任任何代码。

      只需使用auto 就可以告诉编译器和维护者您打算声明一个新变量。

      想一想,为了避免这种噩梦,FORTRAN 中引入了“隐式无”语句 - 现在您会看到它在所有严肃的 FORTRAN 程序中都使用过。没有它简直……可怕。

      【讨论】:

        猜你喜欢
        • 2014-05-29
        • 2016-07-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-05-20
        • 1970-01-01
        • 1970-01-01
        • 2012-01-30
        相关资源
        最近更新 更多