Programing

실제로 오버로드 된 && 및 || 이유가 있습니까?

crosscheck 2020. 6. 25. 08:14
반응형

실제로 오버로드 된 && 및 || 이유가 있습니까? 단락하지 않습니까?


짧은 단락의 운영자의 행동 &&과는 ||프로그래머를위한 놀라운 도구입니다.

그러나 오버로드 될 때 왜이 동작이 손실됩니까? 연산자는 함수의 구문 설탕 일뿐이지만 연산자 bool는이 동작 가지고 있습니다. 왜이 단일 유형으로 제한되어야합니까? 이것 뒤에 기술적 추론이 있습니까?


모든 설계 프로세스는 서로 호환되지 않는 목표간에 타협을 초래합니다. 불행히도 &&C ++에서 오버로드 된 운영자를 위한 설계 프로세스 는 혼란스러운 최종 결과를 &&낳았습니다. 단락 동작 에서 원하는 기능 은 생략되었습니다.

그 디자인 프로세스에 대한 세부 사항은 내가 알지 못하는 불행한 곳에서 끝났습니다. 그러나 나중의 설계 프로세스가 어떻게이 불쾌한 결과를 고려했는지를 보는 것이 적절합니다. C #에서 오버로드 된 &&작업자 단락되었습니다. C #의 디자이너는 어떻게 그것을 달성 했습니까?

다른 답변 중 하나는 "람다 리프팅"을 제안합니다. 그건:

A && B

도덕적으로 동등한 것으로 실현 될 수 있습니다.

operator_&& ( A, ()=> B )

여기서 두 번째 논증은 평가할 때 부작용과 표현의 가치가 만들어 지도록 게으른 평가를위한 메커니즘을 사용합니다. 오버로드 된 연산자의 구현은 필요할 때만 지연 평가를 수행합니다.

이것이 C # 디자인 팀이 한 것이 아닙니다. (외부 : 람다 리프팅 연산자 표현 트리 표현 을 할 때 내가 한 일이지만 ??, 일부 변환 작업은 느리게 수행해야합니다. 그러나 자세하게 설명하는 것은 큰 혼란이 될 것입니다. 작동하지만 우리가 그것을 피하기에 충분히 무겁습니다.)

오히려 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 }
    ...

(실제로 세 개. C #에서는 연산자 false가 제공 되면 연산자 도 제공 true해야하며,이 질문에 대한 답은 "진정한가?"입니다. 일반적으로 이러한 연산자를 하나만 제공 할 이유가 없으므로 C # 둘 다 필요합니다.)

다음 형식의 진술을 고려하십시오.

C cresult = cleft && cright;

컴파일러는이 의사 C #을 작성했다고 생각한대로 이에 대한 코드를 생성합니다.

C cresult;
C tempLeft = cleft;
cresult = C.false(tempLeft) ? tempLeft : C.&(tempLeft, cright);

보다시피, 왼쪽은 항상 평가됩니다. "거짓"인 것으로 판단되면 결과입니다. 그렇지 않으면 오른쪽이 평가되고 열성적인 사용자 정의 연산자 &가 호출됩니다.

||오퍼레이터는 오퍼레이터의 호출에 해당하고 싶어 같이 유사한 방식으로 정의된다 |연산자

cresult = C.true(tempLeft) ? tempLeft : C.|(tempLeft , cright);

네 개의 연산자를 정의하여 - true, false, &|- C #을 당신이 말을뿐만 아니라 수 있습니다 cleft && cright비 단락뿐만 아니라 cleft & cright, 또한 if (cleft) if (cright) ..., 및 c ? consequence : alternativewhile(c), 등등.

이제 모든 설계 프로세스가 타협의 결과라고 말했습니다. 여기서 C # 언어 디자이너는 단락 &&||올바르게 처리했지만 그렇게하려면 2 대신 4 개의 연산자를 오버로드해야하므로 일부 사람들은 혼란스러워합니다. 연산자 true / false 기능은 C #에서 가장 잘 이해되지 않은 기능 중 하나입니다. C ++ 사용자에게 친숙하고 직관적 인 언어를 사용한다는 목표는 단락을 원하고 람다 리프팅 또는 다른 형태의 게으른 평가를 구현하지 않으려는 욕구에 반대했습니다. 그게 합리적인 타협 위치라고 생각하지만, 그것을 실현하는 것이 중요 하다 타협 위치입니다. 그냥 다른 C ++의 설계자가 착륙 한 것보다 위치를 타협하십시오.

그러한 연산자에 대한 언어 디자인의 주제에 관심이 있다면 C #이 이러한 연산자를 nullable 부울에 정의하지 않는 이유에 대한 시리즈를 읽으십시오.

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


요점은 (C ++ 98의 범위 내에서) 오른쪽 피연산자가 오버로드 된 연산자 함수에 인수로 전달된다는 것입니다. 그렇게 하면 이미 평가 된 것 입니다. 이것을 피할 수 있는 operator||()또는 operator&&()코드가 없거나 할 수있는 것은 없습니다.

원래 연산자는 함수가 아니지만 언어의 하위 수준에서 구현되기 때문에 다릅니다.

추가 언어 기능은 할 수 오른쪽의 비 평가 피연산자 구문 만들었 을 가능 . 그러나 이것은 의미 론적으로 유용한 몇 가지 경우가 있기 때문에 신경 쓰지 않았습니다 . (과 마찬가지로 ? :오버로드에는 사용할 수 없습니다.

(람다를 표준으로 도입하는 데 16 년이 걸렸습니다 ...)

의미 적 사용에 대해서는 다음을 고려하십시오.

objectA && objectB

이것은 다음과 같이 요약됩니다.

template< typename T >
ClassA.operator&&( T const & objectB )

에 대한 변환 연산자를 호출하는 것 외에 objectB (알 수없는 유형)로 정확히 무엇을하고 싶은지 bool, 언어 정의를 위해 단어에 어떻게 넣었는지 생각해보십시오.

그리고 당신 bool로 변환을 호출 한다면 , 음 ...

objectA && obectB

같은 일을합니까, 이제합니까? 왜 처음에 과부하입니까?


기능은 생각, 설계, 구현, 문서화 및 배송되어야합니다.

이제 우리는 그것을 생각했습니다. 왜 그것이 쉬운 지 (그리고 그렇게하기 어려운 이유)를 보자. 또한 자원의 양이 제한되어 있으므로 추가하면 다른 항목이 잘릴 수 있습니다 (무엇을 원하십니까?).


이론적으로, 모든 연산자 는 C ++ 11 기준 으로 단 하나의 "사소한" 추가 언어 기능 만으로 단락 동작을 허용 할 수 있습니다. C ++ 98 이후) :

C ++은 필요하고 허용 될 때까지 평가를 피하기 위해 인수를 지연 평가 (숨겨진 람다)로 주석을 달 수있는 방법이 필요합니다 (사전 조건이 충족 됨).


이론적 인 기능은 어떤 모습입니까 (새로운 기능은 널리 사용 가능해야 함을 기억하십시오)?

lazy함수 인수에 적용된 주석은 함수를 functor를 기대하는 템플릿으로 만들고 컴파일러가 표현식을 functor로 묶도록합니다.

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

// And be called like
exp_b && exp_c;
// or
operator&&(exp_b, exp_c);

다음과 같이 표지 아래에 표시됩니다.

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;});

람다는 숨겨져 있으며 최대 한 번 호출됩니다. 공통 하위 식 제거 가능성의 감소와는 별도로, 성능 저하
가 없어야합니다 .


구현 복잡성 및 개념적 복잡성 (다른 기능에 대한 복잡성을 충분히 완화하지 않는 한 모든 기능이 모두 증가 함) 외에도 다른 중요한 고려 사항 인 이전 버전과의 호환성을 살펴 보겠습니다.

언어 기능 은 코드를 손상시키지 않지만 코드를 활용하여 API를 미묘하게 변경하므로 기존 라이브러리에서 사용하면 자동 변경이 이루어집니다.

BTW :이 기능은 사용하기는 쉽지만 별도의 정의를 위해 C # 솔루션을 분할 &&하고 ||두 가지 기능 을 사용하는 것보다 엄격 합니다.


회고 적 합리화로 인해 주로

  • (새로운 구문을 도입하지 않고) 단락을 보장하려면 연산자를 다음과 같이 제한해야합니다. 결과실제 첫 번째 인수 컨버터블로 bool하고,

  • 단락은 필요할 때 다른 방식으로 쉽게 표현할 수 있습니다.


예를 들어, 클래스가있는 경우 T관련있다 &&||사업자, 다음 식

auto x = a && b || c;

여기서 a, bc유형의 표현되고 T, 단락 등으로 표현 될 수있다

auto&& and_arg = a;
auto&& and_result = (and_arg? and_arg && b : and_arg);
auto x = (and_result? and_result : and_result || c);

또는 아마도 더 명확하게

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; }
}();

명백한 중복성은 운영자 호출로 인한 부작용을 유지합니다.


람다 재 작성이 더 장황한 반면, 캡슐화가 향상되면 그러한 연산자 정의 할 수 있습니다 .

나는 다음의 모든 표준 준수를 확신하지는 못하지만 (여전히 약간의 인플루엔자) 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;
            }
        }
    }
}

산출:

000-> !! !! || 그릇된
001-> !! !! || 진실
010 화 !! || 그릇된
011-> !! !! || 진실
100-> !! && !! || 그릇된
101-> !! && !! || 진실
110-> !! && !! 진실
111-> !! && !! 진실

여기서 각 !!bang-bang은 로의 변환 bool, 즉 인수 값 확인 을 보여줍니다 .

컴파일러가 쉽게 동일한 작업을 수행하고 추가로 최적화 할 수 있기 때문에 이는 가능한 구현 방식이며 불가능성에 대한 주장은 일반적으로 불가능 성 주장과 동일한 범주, 즉 일반적으로 볼록과 같은 범주에 있어야합니다.


tl; dr : 높은 비용 (특별 구문 필요)과 비교할 때 수요가 매우 적기 때문에 (누가이 기능을 사용할 것인가?) 노력할 가치가 없습니다.

마음에 오는 첫번째 것은 연산자 오버로딩은 사업자의 부울 버전 반면, 쓰기 기능에 단지 멋진 방법이다 ||&&물건 buitlin 있습니다. 컴파일러는 이들 단락의 자유를 가지며, 식 동안 것을 의미 x = y && znonboolean 함께 yz같은 함수의 호출을 야기한다 X operator&& (Y, Z). , 함수를 호출하기 전에 매개 변수를 모두 평가해야하는 이상한 이름의 함수를 호출 y && z하는 멋진 방법 operator&&(y,z)일뿐입니다 (단락이 적절하다고 간주되는 항목 포함).

그러나, &&연산자 new를 호출 한 operator new다음 생성자 를 호출하는 것으로 번역 된 연산자 와 같이 연산자 의 번역을 좀 더 정교 하게 만들 수 있어야한다고 주장 할 수 있습니다 .

기술적으로 이것은 문제가되지 않습니다. 단락을 가능하게하는 전제 조건에 특정한 언어 구문을 정의해야합니다. 그러나 단락의 사용 Y은 적절한 경우 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);
}

하나는 드물게 과부하 싶어 operator||하고 operator&&드물게 쓰는 경우가 있기 때문에, a && b실제로 것은 nonboolean 맥락에서 직관적이다. 내가 아는 유일한 예외는 표현 템플릿 (예 : 임베디드 DSL)입니다. 그리고 소수의 사례 중 소수만이 단락 평가의 이점을 누릴 수 있습니다. 식 템플릿은 나중에 평가되는 식 트리를 형성하는 데 사용되므로 일반적으로 사용하지 않으므로 항상 식의 양쪽이 필요합니다.

한마디로 : 컴파일러 작가도 표준의 저자도는 만에 하나 정의 된 사용자에 단락이 좋을 것이라고 생각 얻을 수 있습니다, 때문에 농구를 통해 뛰어 정의하고 추가 성가신 구문을 구현해야 할 필요성을 느꼈다 operator&&operator||- 단지 손에 논리를 쓰는 것보다 노력이 적지 않다는 결론에 도달합니다.


논리 연산자는 연관된 진리표의 평가에서 "최적화"이기 때문에 단락이 허용됩니다. 논리 자체 기능 이며이 논리가 정의됩니다.

과부하 이유 실제로 거기 &&||쇼트을은?

사용자 정의 오버로드 된 논리 연산자는 이러한 진리표의 논리를 따를 의무없습니다 .

그러나 오버로드 될 때 왜이 동작이 손실됩니까?

따라서 전체 기능을 정상적으로 평가해야합니다. 컴파일러는이를 일반 과부하 연산자 (또는 함수)로 취급해야하며 다른 함수와 마찬가지로 최적화를 적용 할 수 있습니다.

사람들은 다양한 이유로 논리 연산자를 과부하시킵니다. 예를 들어; 그것들은 사람들이 익숙한 "정상적인"논리적 인 것이 아닌 특정 영역에서 특정한 의미를 가질 수 있습니다.


단락은 "및"및 "또는"의 진리표 때문입니다. 사용자가 어떤 작업을 정의 할 것인지 어떻게 알 수 있으며 두 번째 연산자를 평가할 필요가 없다는 것을 어떻게 알 수 있습니까?


게으름을 일으키는 유일한 방법은 Lambdas가 아닙니다. 게으른 평가는 C ++의 Expression Templates사용하여 비교적 간단 합니다. 키워드가 필요 없으며 lazyC ++ 98에서 구현할 수 있습니다. 식 트리는 이미 위에서 언급했습니다. 식 템플릿은 가난하지만 영리한 사람의 식 나무입니다. 트릭은 표현식을 재귀 적으로 중첩 된 Expr템플릿 인스턴스화 트리로 변환하는 것 입니다. 나무는 시공 후 별도로 평가됩니다.

다음 코드 무료 기능을 제공 하고 로 변환 할 수있는 한 단락 &&||연산자 클래스 S구현 합니다 . 코드는 C ++ 14에 있지만 아이디어는 C ++ 98에도 적용됩니다. 라이브 예를 참조하십시오 .logical_andlogical_orbool

#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;
}

but the operators for bool have this behaviour, why should it be restricted to this single type?

I just want to answer this one part. The reason is that the built-in && and || expressions are not implemented with functions as overloaded operators are.

Having the short-circuiting logic built-in to the compiler's understanding of specific expressions is easy. It's just like any other built-in control flow.

But operator overloading is implemented with functions instead, which have particular rules, one of which is that all the expressions used as arguments get evaluated before the function is called. Obviously different rules could be defined, but that's a bigger job.

참고URL : https://stackoverflow.com/questions/25913237/is-there-actually-a-reason-why-overloaded-and-dont-short-circuit

반응형