【问题标题】:De Morgan's Law optimization with overloaded operators重载运算符的德摩根定律优化
【发布时间】:2016-01-12 06:05:18
【问题描述】:

每个程序员都应该知道:



(De Morgan's Laws)

在某些情况下,为了优化程序,编译器可能会修改(!p && !q)(!(p || q))

这两个表达式是等价的,计算第一个或第二个没有区别。
但是在 C++ 中,可以重载运算符,并且重载的运算符可能并不总是尊重这个属性。所以以这种方式转换代码实际上会修改代码。

!||&& 重载时,编译器是否应该使用德摩根定律?

【问题讨论】:

  • 任何理智的编译器编写者都避免相信程序员已经正确地实现了逆运算符。不这样做是一个非常常见的错误。
  • 一般来说,如果这些转换不会改变程序的可观察行为(副作用、输出),编译器只能将这些转换应用于您的程序。当pq 是布尔原语时,当然可以应用德摩根定律,因为这不会改变可观察到的行为。当pq 有重载的运算符时,这可能是也可能不是。 C++ 标准对德摩根定律只字未提。编译器只有在知道它不会改变行为的情况下才被“允许”使用它。
  • 如果我在 15 位程序员的办公室里走来走去,让他们中的任何一个说出一条德摩根定律,他们将无法做到。所以“每个程序员都应该知道”的说法有点误导......
  • @corsiKa:“应该”和“将”是两个截然不同的词
  • @corsiKa:每个 C++ 程序员都应该能够,给定代码 if (p || q) { f(); } else { g(); } 能够回答“在什么条件下调用 g()?”有人可能会说“当(p || q) 为假时”,但大多数人可以应用德摩根定理并知道“如果p 或q 为真,则调用f(),当p 和q 都为假时g()”这就是了解规律,即使他们不叫他们的名字。

标签: c++ operator-overloading language-lawyer compiler-optimization


【解决方案1】:

我认为您已经回答了您自己的问题:不,编译器无法做到这一点。不仅操作符可以重载,有些甚至不能定义。例如,您可以定义 operator &&operator !,而根本不定义 operator ||

请注意,编译器无法遵循许多其他规律。比如不能将p||q改成q||p,也不能把x+y改成y+x

(以上所有内容都适用于重载运算符,因为这是问题所要求的。)

【讨论】:

  • 如果你说的是不重载,编译器可以将 p||q 更改为 q||p 如果它可以证明 q 没有副作用或未定义的行为,如果 p 为 false,并且 p 有如果 q 为假,则没有副作用。所以 if (x >= 0 && x
  • @gasher729,更新了答案以表明这是关于重载运算符,因为这就是问题所要求的。
  • @gasher729 您所说的是规范中允许实现必须“仅模拟可观察行为”的结果。当然,它可以重新排列 'q || p' 如果它可以证明结果(包括副作用)是完全等价的,但鉴于结果 is then 完全等价,这种重新排列没有可观察到的效果。我认为,OP 提出的问题是专门询问 确实 改变可观察行为的重新安排。
【解决方案2】:

不,在这种情况下,转换将无效。根据 as-if 规则,将 !p && !q 转换为 !(p || q) 的权限是隐含的。 as-if 规则允许任何转换,粗略地说,不能被正确的程序观察到。当使用重载运算符并检测到转换时,这自动意味着不再允许转换。

【讨论】:

  • 除非编译器分析了重载的运算符并确定 as-if 规则仍然适用。编译器甚至不太可能尝试:-)
  • @gnasher729:编译器会分析重载的运算符,但不是为了进行这种转换。内联后,它可能会发现有机会转换内置类型上的操作,冗余代码折叠可能会导致它重用在互补运算符中实现的分支......但这都不是明确尝试应用DeMorgan 定理到用户定义的运算符。
  • @BenVoigt 实际上,当我阅读该评论时,我认为即使当前的编译器不这样做,未来的编译器也可以合法地尝试,这似乎是一个有趣的启发式方法:看看如果这种转换是否会出现优化机会执行,如果是,则检查运算符是否以允许该转换的方式实现。对于一些非常具体的程序,它可能非常有用,如果它不会明显损害编译器的整体性能,那么这可能足以让它保持这种状态。 (我也认为这不太可能,但我还是喜欢它。)
【解决方案3】:

注意:

内置运算符 && 和 ||执行短路评估(如果在评估第一个操作数后结果已知,则不评估第二个操作数),但重载运算符的行为类似于常规函数调用并且始终评估两个操作数

... 因为 operator&& 和 operator|| 的短路性质不适用于重载,并且由于具有布尔语义的类型并不常见,因此只有两个标准库类重载了这些运算符...

来源:http://en.cppreference.com/w/cpp/language/operator_logical (强调我的)

还有:

如果有同名用户写的候选人 和参数类型作为内置候选算子函数,内置算子函数被隐藏和 不包含在候选函数集中。

来源:n4431 13.6 内置运算符 [over.built](强调我的)

总结一下:重载运算符的行为类似于常规的、用户编写的函数。

不,编译器不会将用户编写的函数的调用替换为另一个用户编写的函数的调用。 否则可能会违反"as if" 规则。

【讨论】:

    【解决方案4】:

    重载运算符本身只是函数调用的语法糖;不允许编译器本身对此类调用可能持有或不持有的属性做出任何假设。利用某些特定运算符的属性的优化(例如,布尔运算符的 De Morgan's,和的交换性,和/积的分布性,适当乘法中的积分除法转换......)只有在“真正的运算符”时才能使用被使用了。

    请注意,标准库的某些部分可能会将某些特定的语义含义与重载的运算符相关联 - 例如,std::sort 默认情况下需要遵守严格的弱排序之间的 operator<元素 - 但这当然列在每个算法/容器的先决条件中。

    (顺便说一句,重载&&|| 无论如何都应该避免,因为它们在重载时会失去短路特性,因此它们的行为会变得令人惊讶并因此具有潜在危险)

    【讨论】:

      【解决方案5】:

      你在问编译器是否可以任意重写你的程序来做你没有写的事情。

      答案是:当然不是!

      • De Morgan 的法律适用的地方也可能适用。
      • 如果他们不这样做,他们可能不会。

      就这么简单。

      【讨论】:

      • “你在问编译器是否可以任意重写你的程序来做你没有写的事情” - 不,OP是在询问编译器是否可以在特定情况下应用特定的转换。
      • @davmac 对,这种特定的转换将是对程序的任意重写,以执行 OP 未编写的操作。
      • 不,这将是特定情况下程序的特定(非任意)转换(“重写”)。 OP 询问该语言是否允许编译器执行此转换。这与询问编译器是否可以对程序进行任意更改不同。此外,“OP 没有写它来做”的事情取决于语言的实际规则。 C++ 标准可能允许这样的转换;碰巧的是,它没有-这是OP所要求的。答案只有在你已经知道的情况下才会显而易见。
      • 特别是,C++ 规范有可能甚至合理地要求这些运算符在重载时必须以尊重德摩根定律的方式编写。碰巧的是,规范并不要求这样做(这使得将这些运算符用于逻辑合取、析取和否定之外的其他事情的可能性打开了,例如在解析器组合器 DSL 或类似的东西中),因此优化是无效的。
      • 就像 C++ 有一个特定的规则允许编译器在某些情况下删除副本一样,即使当复制操作是用户定义时这可能会改变程序的行为,它也可以有一个特定的规则这允许编译器应用某些特定的转换,例如 De Morgan 定律或将 x != y 替换为 !(x == y)。它没有这样的规则,但这并不明显。
      【解决方案6】:

      不直接。

      如果 p 和 q 是表达式,因此 p 没有重载运算符,则短路评估有效:仅当 p 为 false 时才会评估表达式 q。

      如果 p 是非原始类型,则没有短路评估,并且重载函数可以是任何东西 - 甚至与常规用法无关。

      编译器会以自己的方式进行优化。也许它可能会导致德摩根恒等式,但不是在 if 条件替换的级别上。

      【讨论】:

        【解决方案7】:

        DeMorgan 定律适用于这些运算符的语义。重载适用于这些运算符的语法。无法保证重载的运算符实现了应用德摩根定律所需的语义。

        【讨论】:

        • “无法保证重载的运算符实现了德摩根定律应用所需的语义” - OP 在问题中承认了这一点。我想你误解了 OP 想要问什么。
        • @davmac - 我想你误解了我的回答。
        • 我想这是可能的 - 需要详细说明吗? OP 非常清楚地认识到重载运算符可能无法实现 DeMorgan 身份应用所需的语义。我相信 OP 正在询问是否允许编译器根据这些身份转换表达式,而不管重载以及身份是否实际成立(这无疑是一种解释,而不是所问问题的字面意思)。你的回答似乎没有解决这个问题。那么,您只是在解决字面上的问题吗?
        【解决方案8】:

        但是在 C++ 中可以重载运算符,而重载的运算符可能并不总是尊重这个属性。

        重载的运算符不再是运算符,而是函数调用。

        class Boolean
        {
          bool value;
        
          ..
        
          Boolean operator||(const Boolean& b)
          {
              Boolean c;
              c.value = this->value || b.value;
              return c;
          }
        
          Boolean logical_or(const Boolean& b)
          {
              Boolean c;
              c.value = this->value || b.value;
              return c;
          }
        }
        

        所以这行代码

        Boolean a (true);
        Boolean b (false);
        
        Boolean c = a || b;
        

        等价于这个

        Boolean c = a.logical_or(b);
        

        【讨论】:

          猜你喜欢
          • 2017-07-16
          • 2017-05-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-01-10
          • 1970-01-01
          • 2016-03-13
          相关资源
          最近更新 更多