是否存在重载&&和||的原因 不要短路?

iFr*_*cht 136 c++ operator-overloading short-circuiting logical-operators c++11

运算符的短路行为&&,并||是程序员的好工具.

但为什么他们在超载时会失去这种行为?我理解运算符只是函数的语法糖,但操作符bool有这种行为,为什么它应该限制在这种类型?这背后有任何技术推理吗?

Eri*_*ert 151

所有设计过程都会导致互不相容的目标之间的妥协.不幸的是,&&C++中重载运算符的设计过程产生了一个令人困惑的最终结果:&&省略了你想要的功能- 它的短路行为.

这个设计过程如何在这个不幸的地方结束的细节,我不知道的.然而,有必要了解后来的设计过程如何将这种不愉快的结果考虑在内.在C#中,重载的&&运算符短路的.C#的设计者是如何实现这一目标的?

其他答案之一暗示"lambda lift".那是:

A && B
Run Code Online (Sandbox Code Playgroud)

可以被认为是道德上等同于:

operator_&& ( A, ()=> B )
Run Code Online (Sandbox Code Playgroud)

其中第二个参数使用某种机制进行延迟评估,以便在评估时生成表达式的副作用和值.重载运算符的实现只会在必要时进行惰性求值.

这不是C#设计团队所做的.(旁白:虽然lambda提升我在做表达树表示??操作员时所做的,这需要懒惰地执行某些转换操作.然而,详细描述这将是一个主要的题外话.我只想说:lambda提升工作,但足够重量,我们希望避免它.)

相反,C#解决方案将问题分解为两个独立的问题:

  • 我们应该评估右手操作数吗?
  • 如果上面的答案是"是",那么我们如何组合这两个操作数呢?

因此,通过&&直接过载是非法的来解决问题.相反,在C#中,您必须重载两个运算符,每个运算符都回答这两个问题中的一个.

class C
{
    // Is this thing "false-ish"? If yes, we can skip computing the right
    // hand size of an &&
    public static bool operator false (C c) { whatever }

    // If we didn't skip the RHS, how do we combine them?
    public static C operator & (C left, C right) { whatever }
    ...
Run Code Online (Sandbox Code Playgroud)

(旁白:实际上,有三个.C#要求如果false提供运算符,那么true还必须提供运算符,它回答了这个问题:这个东西是"真的吗?".通常没有理由只提供一个这样的运算符,所以C#需要两者.)

考虑一下表格的陈述:

C cresult = cleft && cright;
Run Code Online (Sandbox Code Playgroud)

编译器为此生成代码,因为您已经编写了这个伪C#:

C cresult;
C tempLeft = cleft;
cresult = C.false(tempLeft) ? tempLeft : C.&(tempLeft, cright);
Run Code Online (Sandbox Code Playgroud)

如您所见,始终评估左侧.如果确定它是"假的",那么结果就是这样.否则,将评估右侧,并调用急切的用户定义运算符&.

||操作者在类似的方式定义的,如操作者的调用真实和渴望|操作者:

cresult = C.true(tempLeft) ? tempLeft : C.|(tempLeft , cright);
Run Code Online (Sandbox Code Playgroud)

通过定义所有四个运营商- ,,true 和- C#允许你不仅说,而且非短路,而且,和和,等等.false&|cleft && crightcleft & crightif (cleft) if (cright) ...c ? consequence : alternativewhile(c)

现在,我说所有的设计过程都是妥协的结果.在这里,C#语言设计人员设法得到了短路&&||正确,但这样做需要重载四个运算符而不是两个,有些人会觉得困惑.运算符true/false功能是C#中最不被理解的功能之一.拥有C++用户熟悉的合理且直截了当的语言的目标是反对短路的愿望以及不实施lambda提升或其他形式的懒惰评估的愿望.我认为这是一个合理的妥协立场,但重要的是要认识到这一个妥协的立场.与C++的设计者相比,只是一个不同的折衷方案.

如果这些运算符的语言设计主题让您感兴趣,请考虑阅读我的系列文章,了解为什么C#不能在可空的布尔值上定义这些运算符:

http://ericlippert.com/2012/03/26/null-is-not-false-part-one/

  • 在这种情况下,我认为妥协是有道理的.复杂的东西只是类库的架构师必须关注的东西,并且为了换代这种复杂性,它使库的*消耗*更容易和更直观. (5认同)
  • 微软团队没有得到足够的信任:(1)做出明显的努力,在C#中做正确的事情,以及(2)更好地完成正确的事情. (5认同)
  • @Voo:如果你选择实现隐式转换为`bool`那么你可以使用`&&`和`||`而不在C#中实现`operator true/false`或`operator&/ |`没问题.问题出现的原因是*没有转换为`bool`可能*,或者不需要转换. (2认同)

Dev*_*lar 43

关键是(在C++ 98的范围内)右侧操作数将作为参数传递给重载的操作符函数.这样做,它已经被评估了.没有什么对operator||()operator&&()代码可以或不可以做,就可以避免这种情况.

原始运算符是不同的,因为它不是一个函数,而是在较低级别的语言中实现.

其他语言功能可能已经做出了正确的操作数语法的非评估可能的.然而,他们并没有打扰,因为只有极少数情况下这在语义上是有用的.(就像? :,根本不能用于重载.

(他们用了16年的时间才把lambdas变成标准......)

至于语义用法,请考虑:

objectA && objectB
Run Code Online (Sandbox Code Playgroud)

这归结为:

template< typename T >
ClassA.operator&&( T const & objectB )
Run Code Online (Sandbox Code Playgroud)

想想在这里你想用objectB(未知类型)做什么,除了调用转换操作符bool,以及如何将它放入语言定义的单词中.

而且如果打电话到bool的转换,以及...

objectA && obectB
Run Code Online (Sandbox Code Playgroud)

做同样的事情,现在呢?那么为什么首先要超负荷?

  • @iFreilicht:任何一个类的`bool`转换运算符也可以访问所有成员变量,并且可以与内置运算符一起使用.还有其他*但*转换为bool对于短路评估无论如何都不具备语义意义!试着从语义的角度来看待这个问题,而不是语法问题:*你想要实现什么*,而不是*你将如何实现它. (9认同)
  • @iFreilicht:相反,短路的目的是*因为左手边的计算可以确定右手边*的前提条件的真实性.`if(x!= NULL && x-> foo)`需要短路,不是为了速度,而是为了安全. (8认同)
  • 那么你的逻辑错误就是在当前定义的语言中推断出不同定义语言的效果.在过去,许多新手曾经这样做过."虚拟构造函数".他们花了大量的解释才能让他们摆脱这种思维方式.无论如何,通过内置运算符的短路,可以保证参数非评估.如果为它们定义了短路,那么这种保证也会出现在用户定义的过载中. (7认同)

Ded*_*tor 26

必须考虑,设计,实施,记录和发送功能.

现在我们想到了它,让我们看看为什么它现在可能很容易(而且很难做到).另外请记住,只有有限的资源,所以添加它可能会削减其他东西(你想放弃什么?).


从理论上讲,所有操作员都可以允许短路行为只有一个"次要的" 附加语言特征,从C++ 11开始(当lambdas被引入时,在"C with classes"开始于1979年之后32年,仍然可敬16在c ++ 98之后):

C++只需要一种方法来将一个参数注释为延迟评估 - 一个隐藏的lambda - 以避免评估直到必要和允许(满足前提条件).


这个理论特征会是什么样的(请记住,任何新功能都应该广泛使用)?

lazy应用于函数参数的注释使函数成为期望仿函数的模板,并使编译器将表达式打包为仿函数:

A operator&&(B b, __lazy C c) {return c;}

// And be called like
exp_b && exp_c;
// or
operator&&(exp_b, exp_c);
Run Code Online (Sandbox Code Playgroud)

它会在封面下看起来像:

template<class Func> A operator&&(B b, Func& f) {auto&& c = f(); return c;}
// With `f` restricted to no-argument functors returning a `C`.

// And the call:
operator&&(exp_b, [&]{return exp_c;});
Run Code Online (Sandbox Code Playgroud)

请特别注意lambda保持隐藏状态,最多只调用一次.
除此之外,应该没有性能降低,除了减少共同子表达消除的机会.


除了实现复杂性和概念复杂性(每个功能都增加了,除非它足以简化其他功能的复杂性),让我们看看另一个重要的考虑因素:向后兼容性.

虽然这种语言功能不会破坏任何代码,但它会巧妙地改变任何利用它的API,这意味着在现有库中的任何使用都将是一个无声的突破性变化.

BTW:这个功能虽然更易于使用,但它比分裂的C#解决方案更强大,&&并且||分别用于单独定义的两个函数.

  • @iFreilicht:任何形式的问题"为什么X不存在?" 具有相同的答案:存在该功能必须被认为是一个好主意,设计,指定,实施,测试,记录,并运送给最终用户.如果没有发生任何一件事,没有任何功能.你提出的功能没有发生其中一件事; 找出哪一个是历史研究问题; 如果您关心哪一件事从未完成,请开始与设计委员会的人员交谈. (6认同)

Che*_*Alf 13

回顾性合理化,主要是因为

  • 为了保证短路(不引入新语法),必须限制运营商 结果实际的第一个参数可转换为bool,和

  • 在需要时,可以通过其他方式容易地表达短路.


例如,如果一个类T具有关联&&||运算符,那么表达式

auto x = a && b || c;
Run Code Online (Sandbox Code Playgroud)

其中a,b并且c是类型的表达式T,可以用短路表示

auto&& and_arg = a;
auto&& and_result = (and_arg? and_arg && b : and_arg);
auto x = (and_result? and_result : and_result || c);
Run Code Online (Sandbox Code Playgroud)

或者更明确的是

auto x = [&]() -> T_op_result
{
    auto&& and_arg = a;
    auto&& and_result = (and_arg? and_arg && b : and_arg);
    if( and_result ) { return and_result; } else { return and_result || b; }
}();
Run Code Online (Sandbox Code Playgroud)

明显的冗余保留了操作员调用的任何副作用.


虽然lambda重写更详细,但其更好的封装允许人们定义这样的运算符.

我不完全确定以下所有内容的标准一致性(仍然有点影响),但它使用Visual C++ 12.0(2013)和MinGW g ++ 4.8.2完全编译:

#include <iostream>
using namespace std;

void say( char const* s ) { cout << s; }

struct S
{
    using Op_result = S;

    bool value;
    auto is_true() const -> bool { say( "!! " ); return value; }

    friend
    auto operator&&( S const a, S const b )
        -> S
    { say( "&& " ); return a.value? b : a; }

    friend
    auto operator||( S const a, S const b )
        -> S
    { say( "|| " ); return a.value? a : b; }

    friend
    auto operator<<( ostream& stream, S const o )
        -> ostream&
    { return stream << o.value; }

};

template< class T >
auto is_true( T const& x ) -> bool { return !!x; }

template<>
auto is_true( S const& x ) -> bool { return x.is_true(); }

#define SHORTED_AND( a, b ) \
[&]() \
{ \
    auto&& and_arg = (a); \
    return (is_true( and_arg )? and_arg && (b) : and_arg); \
}()

#define SHORTED_OR( a, b ) \
[&]() \
{ \
    auto&& or_arg = (a); \
    return (is_true( or_arg )? or_arg : or_arg || (b)); \
}()

auto main()
    -> int
{
    cout << boolalpha;
    for( int a = 0; a <= 1; ++a )
    {
        for( int b = 0; b <= 1; ++b )
        {
            for( int c = 0; c <= 1; ++c )
            {
                S oa{!!a}, ob{!!b}, oc{!!c};
                cout << a << b << c << " -> ";
                auto x = SHORTED_OR( SHORTED_AND( oa, ob ), oc );
                cout << x << endl;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

000 -> !! !! || false
001 -> !! !! || true
010 -> !! !! || false
011 -> !! !! || true
100 -> !! && !! || false
101 -> !! && !! || true
110 -> !! && !! true
111 -> !! && !! true

这里每个!!bang-bang显示转换bool,即参数值检查.

由于编译器可以轻松地做同样的事情,并且另外对其进行优化,因此这是一种已证明的可能的实现方式,并且任何不可能性的声明必须与一般的不可能性声明(即,通常为bollocks)属于同一类别.


Sum*_*ant 6

Lambda 并不是引入惰性的唯一方法。使用C++ 中的表达式模板进行延迟评估相对简单。不需要关键字lazy,可以在C++98中实现。上面已经提到了表达式树。表达模板是可怜的(但聪明的)人的表达树。诀窍是将表达式转换为Expr模板的递归嵌套实例化树。树在构建后单独评估。

以下代码为 class实现了短路&&||运算符,S只要它提供logical_andlogical_or免费功能,并且可以转换为bool. 代码在 C++14 中,但这个想法也适用于 C++98。请参阅现场示例

#include <iostream>

struct S
{
  bool val;

  explicit S(int i) : val(i) {}  
  explicit S(bool b) : val(b) {}

  template <class Expr>
  S (const Expr & expr)
   : val(evaluate(expr).val)
  { }

  template <class Expr>
  S & operator = (const Expr & expr)
  {
    val = evaluate(expr).val;
    return *this;
  }

  explicit operator bool () const 
  {
    return val;
  }
};

S logical_and (const S & lhs, const S & rhs)
{
    std::cout << "&& ";
    return S{lhs.val && rhs.val};
}

S logical_or (const S & lhs, const S & rhs)
{
    std::cout << "|| ";
    return S{lhs.val || rhs.val};
}


const S & evaluate(const S &s) 
{
  return s;
}

template <class Expr>
S evaluate(const Expr & expr) 
{
  return expr.eval();
}

struct And 
{
  template <class LExpr, class RExpr>
  S operator ()(const LExpr & l, const RExpr & r) const
  {
    const S & temp = evaluate(l);
    return temp? logical_and(temp, evaluate(r)) : temp;
  }
};

struct Or 
{
  template <class LExpr, class RExpr>
  S operator ()(const LExpr & l, const RExpr & r) const
  {
    const S & temp = evaluate(l);
    return temp? temp : logical_or(temp, evaluate(r));
  }
};


template <class Op, class LExpr, class RExpr>
struct Expr
{
  Op op;
  const LExpr &lhs;
  const RExpr &rhs;

  Expr(const LExpr& l, const RExpr & r)
   : lhs(l),
     rhs(r)
  {}

  S eval() const 
  {
    return op(lhs, rhs);
  }
};

template <class LExpr>
auto operator && (const LExpr & lhs, const S & rhs)
{
  return Expr<And, LExpr, S> (lhs, rhs);
}

template <class LExpr, class Op, class L, class R>
auto operator && (const LExpr & lhs, const Expr<Op,L,R> & rhs)
{
  return Expr<And, LExpr, Expr<Op,L,R>> (lhs, rhs);
}

template <class LExpr>
auto operator || (const LExpr & lhs, const S & rhs)
{
  return Expr<Or, LExpr, S> (lhs, rhs);
}

template <class LExpr, class Op, class L, class R>
auto operator || (const LExpr & lhs, const Expr<Op,L,R> & rhs)
{
  return Expr<Or, LExpr, Expr<Op,L,R>> (lhs, rhs);
}

std::ostream & operator << (std::ostream & o, const S & s)
{
  o << s.val;
  return o;
}

S and_result(S s1, S s2, S s3)
{
  return s1 && s2 && s3;
}

S or_result(S s1, S s2, S s3)
{
  return s1 || s2 || s3;
}

int main(void) 
{
  for(int i=0; i<= 1; ++i)
    for(int j=0; j<= 1; ++j)
      for(int k=0; k<= 1; ++k)
        std::cout << and_result(S{i}, S{j}, S{k}) << std::endl;

  for(int i=0; i<= 1; ++i)
    for(int j=0; j<= 1; ++j)
      for(int k=0; k<= 1; ++k)
        std::cout << or_result(S{i}, S{j}, S{k}) << std::endl;

  return 0;
}
Run Code Online (Sandbox Code Playgroud)


nj-*_*ath 5

短路是因为“与”和“或”的真值表。你怎么知道用户要定义什么操作,你怎么知道你不必评估第二个操作符?


Arn*_*rtz 5

tl; dr:由于需求非常低(谁会使用这个功能?)与相当高的成本(需要特殊语法)相比,这是不值得的.

,想到的第一件事是,操作符重载只是写功能,而运营商的布尔版本奇特的方式||&&被buitlin东西.这意味着编译器可以自由地将它们短路,而表达式x = y && z使用非boolean y并且z必须导致调用类似的函数X operator&& (Y, Z).这意味着这y && z只是一种奇特的写入方式,operator&&(y,z)它只是一个奇怪命名函数的调用,其中两个参数都必须在调用函数之前进行评估(包括任何可能认为是短路的东西).

但是,人们可能会争辩说,应该可以使&&运算符的转换更复杂一些,就像new运算符被转换为调用函数operator new后跟构造函数调用一样.

从技术上讲,这没有问题,人们必须定义一种特定于支持短路的前提条件的语言语法.然而,使用短路将被限制到案件Y是convetible来X,否则必须有如何真正做到短路附加信息(即计算从只有第一个参数的结果).结果必须看起来像这样:

X operator&&(Y const& y, Z const& z)
{
  if (shortcircuitCondition(y))
    return shortcircuitEvaluation(y);

  <"Syntax for an evaluation-Point for z here">

  return actualImplementation(y,z);
}
Run Code Online (Sandbox Code Playgroud)

一个罕想超载operator||operator&&,因为罕在这里写的情况下,a && b实际上是在nonboolean背景下直观.我所知道的唯一例外是表达式模板,例如嵌入式DSL.只有少数几个案例可以从短路评估中受益.表达式模板通常不会,因为它们用于形成稍后评估的表达式树,因此您始终需要表达式的两侧.

简而言之:无论编译器作者也没有标准,作者认为有必要跳火圈和定义和实现额外的繁琐的语法,只因为百万分之一可能的想法,这将是不错的用户短路定义operator&&operator||-只是得出的结论是,每手写逻辑并不是一件容易的事.


Nia*_*all 5

允许短路逻辑运算符,因为它是评估相关真值表的"优化".它是逻辑本身的一个功能,并且定义了这个逻辑.

实际上是否存在过载&&||不短路的原因?

自定义重载逻辑运算符没有义务遵循这些真值表的逻辑.

但为什么他们在超载时会失去这种行为?

因此,整个功能需要按照正常情况进行评估.编译器必须将其视为普通的重载操作符(或函数),它仍然可以像对待任何其他函数一样应用优化.

由于各种原因,人们使逻辑运算符超载.例如; 它们可能在特定领域具有特定含义,而不是人们习以为常的"正常"逻辑.