带重载算子的De Morgan定律优化


67

每个程序员都应该知道:

德摩根1
德摩根2
德摩根定律)

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

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

当和重载时!,编译器是否应使用De Morgan定律?||&&


11
任何理智的编译器编写者都应避免相信程序员已经正确实现了逆运算符。不这样做是一个非常常见的错误。
汉斯·帕桑

6
通常,编译器只有在不改变程序的可观察行为(副作用,输出)的情况​​下,才能将其应用于您的程序。当pq是布尔型原语时,可以确定地应用De Morgan定律,因为这不会改变可观察的行为。当p并且q有重载的运算符时,这可能是正确的,也可能不是。C ++标准并没有说明De Morgan的定律。仅由于知道编译器不会改变行为,才“允许”编译器使用它。
davmac

9
如果我绕过由15名程序员组成的办公室,并要求他们中的任何一个仅列举一个De Morgan法则,那么他们将无能为力。因此,“每个程序员都应该知道”的说法有点误导……
corsiKa

4
@corsiKa:“应该”和“将”是两个截然不同的词
乔纳森·

5
@corsiKa:每个C ++程序员都应该能够,给定的代码if (p || q) { f(); } else { g(); }能够回答“在什么条件下被g()调用?” 有人可能会说“ when (p || q)is false”,但是大多数人都可以应用DeMorgan定理,并且知道“f()如果p或q均为真,则g()当p和q均为假时就会被调用”,这就是定律,即使他们不称呼它们按名字。
Ben Voigt 2015年

Answers:


78

注意:

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

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

资料来源:http : //en.cppreference.com/w/cpp/language/operator_ologic (重点是我的)

然后:

如果存在与内置候选运算符功能具有相同名称和参数类型的用户编写的候选,则内置运算符功能将被隐藏,并且不包含在候选功能集中。

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

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

不,编译器不会用另一个用户编写的函数来代替用户编写的函数。否则可能会违反“好像”规则。


18

我认为您已经回答了自己的问题:不,编译器无法执行此操作。不仅运算符可以重载,有些甚至无法定义。例如,您可以拥有operator &&operator !定义,以及operator ||根本没有定义。

请注意,还有许多其他法律是编译器无法遵循的。例如,它不能改变p||qq||p,以及x+yy+x

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


1
如果您要说的是不重载,则编译器可以证明p没有错误,如果p为假,则可以将p || q更改为q || p,如果q为假,则可以证明p没有副作用。因此,如果(x> = 0 && x <= 100)可以交换,但是如果(p!= NULL && * p == 1)不能交换。
gnasher729

@ gnasher729,更新了答案,以表明这与重载运算符有关,因为这是问题的根源。
彼得

3
@ gnasher729您在说的是规范中允许实现的结果,即实现必须“仅模拟可观察的行为”。当然,它可以重新排列'q || P”,如果它能够证明结果(包括副作用)是完全等效的,但考虑到结果然后完全等效,该重新布置具有没有可观察到的效果。我认为OP提出的问题是专门询问重新安排的问题,这确实改变了可观察的行为。
davmac

9

不,在这种情况下,转换将无效。改造许可!p && !q!(p || q)是隐含的,由AS-if规则。假设规则允许进行任何粗略地说不能由正确程序观察到的转换。当使用重载运算符并检测到转换时,这自动意味着不再允许转换。


2
除非编译器分析了重载的运算符并确定按样规则仍然适用。不太可能编译器甚至尝试:-)
gnasher729

@ gnasher729:编译器将分析重载的运算符,但并非出于此转换的目的。内联后,它可能会发现有机会对内置类型进行转换,并且多余的代码折叠可能会导致其重用互补运算符中实现的分支....但是,这些都不是尝试应用的明确尝试。用户定义的运算符的DeMorgan定理。
Ben Voigt

@BenVoigt实际上,当我阅读该评论时,我认为这似乎是一种有趣的启发,即即使当前的编译器没有机会,未来的编译器也可以合法地尝试:看看是否进行了这种转换会产生优化机会,如果可以,请检查如果运算符以允许该转换的方式实现。对于某些非常特定的程序,它可能非常有用,并且如果它不会显着损害编译器的整体性能,则可能足以保持这种状态。(我确实也不太可能,但是我还是喜欢它。)

5

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

相反,请注意,标准库的某些部分可能会将某些特定的语义与重载的运算符相关联-例如,std::sort默认情况下,期望operator<符合元素之间严格的弱顺序的,但这当然在每种算法/的前提条件中都列出了容器。

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


4

您正在询问编译器是否可以任意重写您的程序以执行您未编写的程序。

答案是:当然不会!

  • 在适用德摩根法律的地方,也可以适用。
  • 如果没有,他们可能不会。

真的就是这么简单。


1
“您正在问编译器是否可以随意重写您的程序以执行您未编写的程序来做” –不,OP正在问编译器是否可以在特定情况下应用特定的转换。
davmac

1
@davmac是的,此特定的转换将是程序的任意重写,以执行OP未编写的操作。
Lightness Races in Orbit

2
不,在特定情况下将是程序的特定(而非任意)转换(“重写”)。OP正在询问该语言是否允许编译器执行此转换。这与询问编译器是否可以对程序进行任意更改不同。同样,“ OP没有编写要执行的操作”取决于语言的实际规则。C ++标准可能允许这样的转换。碰巧的是,事实并非如此-这就是OP的要求。答案只有在您已经知道的情况下才显而易见。
davmac

3
特别是,对于C ++规范,要求那些运算符在重载时必须以尊重De Morgan定律的方式编写,这是可能的,甚至是合理的。碰巧的是,规范不要求这样做(这使得将这些运算符用于除逻辑结合,析取和求反之外的其他用途,例如在解析器组合器DSL或类似的东西中),因此优化无效。
约尔格W¯¯米塔格

4
就像C ++有一条特定的规则允许编译器在某些情况下删除副本一样,即使当用户定义复制操作时这可能会改变程序的行为时,它也可能具有一条特定的规则允许编译器应用某些特定的规则。变换像德·摩根定律或更换x != y!(x == y)。它没有这样的规则,但这并不明显。
CodesInChaos

3

不直接。

如果p和q是表达式,以便p不具有重载运算符,则短路评估有效:仅当p为false时,才对表达式q进行评估。

如果p是非基本类型,则不会进行短路评估,并且过载功能可能是任何东西-甚至与常规用法无关。

编译器将以自己的方式进行优化。也许这可能会导致de Morgan身份,但如果条件替换则不会。


3

DeMorgan的定律适用于这些运算符的语义。重载适用于这些运算符的语法。无法保证重载的运算符会实现DeMorgan律适用的语义。


1
“不能保证过载的运算符会实现应用德摩根定律所需的语义”-OP在问题中承认这一点。我认为您误会了OP想要问的问题。
davmac

@davmac-我认为您误解了我的答案。
皮特·贝克尔

我想这是可能的-需要详细说明吗?OP非常清楚地认识到,重载运算符可能不会实现DeMorgan身份应用所必需的语义。我相信OP正在询问是否允许编译器根据这些身份转换表达式,而不管重载以及该身份是否实际持有(这是一种解释,而不是所问问题的字面意思)。您的答案似乎无法解决这个问题。那么,您只是在解决字面问题吗?
davmac

1

但是在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);
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.