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

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

그러나 오버로드 될 때 왜이 동작이 손실됩니까? 연산자는 함수의 구문 설탕 일 뿐이지 만 연산자 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 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 # 언어 디자이너는 단락 &&||올바르게 처리했지만 사람 대신 네 명의 연산자를 오버로드해야하므로 일부 사람들은 혼란스러워합니다. 연산자 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 기준 으로 단 하나의 “사소한” 추가 언어 기능 만으로 단락 동작을 허용 할 수 있습니다 (람다가 도입 된 시점, 1979 년 “클래스가있는 C”가 시작된 지 32 년이 지난 후 람다는 도입 됨) 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||– 단지 손에 논리를 쓰는 것보다 적은 노력이 아니라는 결론에 도달합니다.


답변

람다는 게으름을 일으키는 유일한 방법은 아닙니다. 게으른 평가는 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;
}


답변

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

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

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

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

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

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