【问题标题】:Expressions with no side effects in C++C++ 中没有副作用的表达式
【发布时间】:2010-11-10 10:41:08
【问题描述】:

看,我不明白的是,为什么像下面这样的程序应该是合法的?

int main()
{ 
    static const int i = 0;
    i < i > i;
}

我的意思是,当然,没有人实际上有任何当前程序中包含没有副作用的表达式,因为那将非常没有意义,而且它会使解析和编译语言变得更加容易。那么为什么不直接禁止他们呢?语言实际上从允许这种语法中获得了什么好处?

另一个例子是这样的:

int main() {
    static const int i = 0;
    int x = (i);
}

这些陈述的实际好处是什么?

还有最令人头疼的解析。有没有人在其他函数中间声明函数?我的意思是,我们摆脱了隐式函数声明之类的东西。为什么不为 C++0x 去掉它们?

【问题讨论】:

    标签: c++ c++11


    【解决方案1】:

    可能是因为禁止这样做会使规范更加复杂,从而使编译器更加复杂。

    【讨论】:

    • 除了如果编译器有两种方法可以解析语句,但其中一种是非法的,那么它知道它必须是另一种,这很简单,而如果标准允许两者,然后编译器必须决定哪个,这远没有那么简单。可行的程序越少,您决定拥有哪些程序所需的逻辑就越少。
    • 但这并不总是正确的,因为编译器需要检测某些无效表达式并报告适当的错误消息。在任何情况下,就像您建议的那样,它当然不能假设代码是有效的。
    【解决方案2】:

    它会使解析和编译 语言更容易

    我不明白怎么做。如果您需要发出诊断,为什么解析和编译i &lt; i &gt; i比解析它更容易,如果您被允许做任何您该死的事情,请提供发出的代码没有副作用?

    Java 编译器禁止无法访问的代码(相对于没有效果的代码),这对程序员来说是喜忧参半,并且需要编译器比 C++ 编译器实际需要做的工作稍微多一点(基本块依赖分析)。 C++ 应该禁止无法访问的代码吗?可能不是。尽管 C++ 编译器确实做了足够的优化来识别无法访问的基本块,但在某些情况下它们可能做得太多。如果foo 是错误的编译时常量,if (foo) { ...} 是否应该是非法的不可访问块?如果它不是编译时常量,但优化器已经弄清楚如何计算该值,它是否合法并且编译器必须意识到它删除它的原因是特定于实现的,以免出错?更多特殊情况。

    实际上没有人有任何电流 具有 no 表达式的程序 副作用

    负载。例如,如果NDEBUG 为真,则assert 扩展为无效表达式,没有任何效果。因此,编译器需要更多的特殊情况来允许 一些 无用的表达式,但不允许其他表达式。

    我相信,其基本原理是,如果它扩展为空,那么 (a) 编译器最终会针对 if (foo) assert(bar); 之类的东西发出警告,并且 (b) 像这样的代码在发布时是合法的,但在调试时是不合法的,这只是令人困惑:

    assert(foo) // oops, forgot the semi-colon
    foo.bar();
    

    最令人头疼的解析之类的事情

    这就是为什么它被称为“烦恼”。这确实是一个向后兼容性问题。如果 C++ 现在改变了那些令人烦恼的解析的含义,那么现有代码的含义就会改变。正如您所指出的,现有代码并不多,但 C++ 委员会在向后兼容性方面采取了相当强硬的立场。如果您想要一种每五分钟更改一次的语言,请使用 Perl ;-)

    无论如何,现在为时已晚。即使我们有一些 C++0x 委员会遗漏的深刻见解,为什么应该删除或不兼容地更改某些功能,它们也不会破坏 FCD 中的任何内容,除非 FCD 明确错误。

    请注意,对于您的所有建议,任何编译器都可能向它们发出警告(实际上,我不明白您对第二个示例的问题是什么,但肯定是因为无用的表达式和函数体中令人烦恼的解析)。如果您是对的,没有人故意这样做,那么警告不会造成任何伤害。如果您认为没有人故意这样做是错误的,那么您声明的删除它们的案例是不正确的。流行编译器中的警告可能会为删除功能铺平道路,特别是因为该标准主要由编译器编写者编写。我们并不总是收到有关这些事情的警告这一事实向我表明,它比您想象的要多。

    【讨论】:

      【解决方案3】:
      • 有时将无用的语句放入程序并对其进行编译以确保它们合法是很方便的 - 例如所涉及的类型可以被解析/匹配等。

      • 尤其是在生成的代码中(宏以及更复杂的外部机制、在某些无操作情况下策略或类型可能会引入无意义扩展的模板),减少特殊的不可编译情况以避免使事情变得更简单

      • 可能有一些临时注释的代码会删除变量的有意义用法,但必须以类似方式识别和注释所有未在其他地方使用的变量可能会很痛苦。

        1234563编译器不知道(例如,外联函数),因此任何好处都仅限于更简单的情况。
      • C++ 需要一个很好的理由来打破向后(并保留 C)兼容性。

      【讨论】:

        【解决方案4】:

        为什么什么都不做被视为特例?此外,虽然上述情况很容易发现,但可以想象更复杂的程序,其中不那么容易识别出没有副作用。

        【讨论】:

          【解决方案5】:

          作为 C++ 标准的迭代,C++0x 必须向后兼容。没有人可以断言您编写的语句不存在于某些由 NASA 或 DoD 编写/拥有的关键软件中。

          无论如何,关于你的第一个例子,解析器不能断言i是一个静态常量表达式,而i &lt; i &gt; i是一个无用的表达式——例如如果i 是模板类型,i &lt; i &gt; i 是“无效变量声明”,不是“无用计算”,也不是解析错误。

          【讨论】:

          • 没有人说 NASA 或 DoD 必须升级到 C++0x。如果他们喜欢它,那么他们可以保留 C++03。旧的编译器版本不会神奇地消失。
          【解决方案6】:

          也许操作符被重载产生了像cout&lt;&lt;i;这样的副作用,这就是现在不能删除它们的原因。另一方面,C# 禁止将非赋值或方法调用表达式用作语句,我相信这是一件好事,因为它使代码更加清晰和语义正确。但是 C# 有机会从一开始就禁止这样做,而 C++ 没有。

          【讨论】:

            【解决方案7】:

            没有副作用的表达式在模板化和宏代码中出现的频率可能比您想象的要高。如果您曾经声明过std::vector&lt;int&gt;,那么您已经实例化了没有副作用的模板代码。 std::vector 必须在释放自身时销毁其所有元素,以防您存储了 T 类型的类。在某些时候,这需要类似于ptr-&gt;~T(); 的语句来调用析构函数。 int 虽然没有析构函数,所以调用没有副作用,将被优化器完全删除。也有可能是在一个循环里面,那么整个循环就没有副作用了,所以整个循环都被优化器去掉了。

            因此,如果您禁止使用没有副作用的表达式,std::vector&lt;int&gt; 将不起作用

            另一个常见的情况是assert(a == b)。在发布版本中,您希望这些断言消失 - 但您不能将它们重新定义为空宏,否则像 if (x) assert(a == b); 这样的语句会突然将下一条语句放入 if 语句中 - 一场灾难!在这种情况下,assert(x) 可以重新定义为((void)0),这是一个没有副作用的语句。现在if 语句在发布版本中也可以正常工作 - 它什么都不做。

            这只是两种常见的情况。还有很多你可能不知道的。因此,虽然没有副作用的表达式看起来是多余的,但它们实际上在功能上很重要。优化器将完全删除它们,因此也不会影响性能。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2019-01-16
              • 2020-10-03
              • 1970-01-01
              • 2021-04-29
              • 2011-09-17
              • 2018-04-13
              相关资源
              最近更新 更多