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


137

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

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


1
@PiotrS. 그 질문은 아마도 답일 것입니다. 나는 표준 이이 목적을 위해 새로운 구문을 정의 할 수 있다고 생각합니다. 아마 operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
— iFreilicht

1
@PiotrS .: 3 상태 논리를 고려하십시오 {true, false, nil}. nil&& x == nil단락 될 수 있기 때문에 .
— MSalters

1
@MSalters : 고려해 봅시다 std::valarray<bool> a, b, c;. 어떻게 a || b || c단락 을 상상 하십니까?
— Piotr Skotnicki

4
@PiotrS. : 단락이 의미 가 있는 비 bool 유형이 하나 이상 있다고 주장합니다 . 나는 단락이 모든 비 부울 유형에 적합하다고 주장하지는 않습니다 .
— MSalters

3
아직 아무도 언급하지 않았지만 이전 버전과의 호환성 문제도 있습니다. 이 단락이 적용되는 상황을 제한하기 위해 특별한주의를 기울이지 않는 한, 그러한 단락은 평가되는 두 피연산자에 과부하가 걸리 operator&&거나 operator||의존 하는 기존 코드를 손상시킬 수 있습니다. 기존 언어에 기능을 추가 할 때 이전 버전과의 호환성을 유지하는 것이 중요합니다.
— David Hammen

답변:


151

모든 설계 프로세스는 서로 호환되지 않는 목표간에 타협을 초래합니다. 불행히도 &&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 : alternative과 while(c), 등등.

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

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

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


1
@Deduplicator :이 질문과 답변을 읽어보실 수도 있습니다 : stackoverflow.com/questions/5965968/…
— Eric Lippert

5
이 경우에는 타협이 정당한 것 이상이라고 생각합니다. 복잡한 것은 클래스 라이브러리의 설계자 만 고려해야 할 사항이며, 이러한 합병증 과 함께 라이브러리 소비 를보다 쉽고 직관적으로 만듭니다.
— 코디 그레이

1
@EricLippert 나는 Envision이 그가이 게시물을보고 그것이 당신이라고 생각했다고 말하고 있다고 생각합니다. 그는 your post무관하다고 말하지 않았습니다 . His noticing your distinct writing style관련이 없습니다.
— WernerCD

5
Microsoft 팀은 (1) C #에서 옳은 일을하기 위해 상당한 노력을 기울이고 (2) 그렇지 않은 경우보다 더 많은 시간을 얻는 것에 대해 충분한 신용을 얻지 못합니다.
— codenheim

2
@Voo : 당신이 암시 적 변환을 구현하도록 선택하면 bool당신은 사용할 수 있습니다 &&및 ||구현없이 operator true/false또는 operator &/|아무런 문제가 C #으로. 문제는 가능한 변환bool 이 없거나 바람직하지 않은 상황 에서 정확하게 발생 합니다.
— Eric Lippert

43

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

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

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

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

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

objectA && objectB

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

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

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

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

objectA && obectB

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


7
논리 오류는 현재 정의 된 언어 내에서 다르게 정의 된 언어의 영향에 대해 추론하는 것입니다. 옛날에는 많은 초보자들이 그 일을 했어요. "가상 생성자". 그런 상자 생각에서 벗어나기 위해 엄청난 양의 설명이 필요했습니다. 어쨌든 내장 연산자의 단락으로 인해 인수 비평가에 대한 보장이 있습니다. 단락이 정의 된 경우 사용자 정의 과부하에도 그러한 보증이 적용됩니다.
— 건배와 hth. -Alf

1
@iFreilicht : 나는 기본적으로 Deduplicator 또는 Piotr과 같은 단어를 다른 단어로 말했습니다. 편집 된 답변에서 요점을 조금 자세히 설명했습니다. 이 방법은 훨씬 더 편리했고, 필요한 언어 확장 (예 : 람다)은 최근까지 존재하지 않았으며 그 혜택은 무시할 수있었습니다. 책임이있는 사람들이 이미하지 않은 것을 "좋아"한 것이 몇 번 수행 1998 년에 다시, 컴파일러 빌더에 의해, 그것은 역효과. (참조 export.)
— DevSolar

9
@iFreilicht : bool두 클래스 의 변환 연산자는 모든 멤버 변수에 액세스 할 수 있으며 내장 연산자와 잘 작동합니다. 다른 건 있지만 변환 - 투 - 부울 어쨌든 단락 회로 평가를 위해 의미 이해가되지 않습니다! 구문 적 관점이 아닌 시맨틱 관점에서 이것에 접근하십시오. 어떻게 하려고 하는지 , 어떻게 하려고하지 않습니까 ?
— DevSolar

1
나는 내가 생각할 수 없다는 것을 인정해야한다. 단락이 존재하는 유일한 이유는 부울 연산 시간을 절약하고 모든 인수를 평가하기 전에 표현식의 결과를 알 수 있기 때문입니다. 다른 AND 연산으로, 이것은 사실이 아니다, 그 이유 즉 &와 &&같은 조작이 아니다. 그것을 깨닫게 도와 주셔서 감사합니다.
— iFreilicht

8
@iFreilicht : 오히려 단락의 목적은 왼쪽의 계산이 오른쪽의 전제 조건의 진실을 확립 할 수 있기 때문 입니다. if (x != NULL && x->foo)속도가 아니라 안전을 위해 단락이 필요합니다.
— Eric Lippert

26

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

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


이론적으로, 모든 연산자 는 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 # 솔루션을 분할 &&하고 ||두 가지 기능 을 사용하는 것보다 엄격 합니다.


6
@iFreilicht : "기능 X가 존재하지 않는 이유는 무엇입니까?" 동일한 답변을 갖습니다. 존재하려면 기능을 생각하고, 좋은 아이디어로 간주하고, 설계, 지정, 구현, 테스트, 문서화하여 최종 사용자에게 제공해야합니다. 이러한 일 중 하나라도 발생하지 않으면 기능이 없습니다. 이러한 기능 중 하나는 제안 된 기능으로 발생하지 않았습니다. 어느 것이 역사적 연구 문제인지 알아 내기; 그 중 하나가 수행되지 않은 것을 신경 쓰면 디자인위원회에서 사람들과 대화를 시작하십시오.
— Eric Lippert

1
@EricLippert : 그리고 어떤 이유인지에 따라 구현 될 때까지 반복하십시오. 아마도 너무 복잡하다고 생각하고 아무도 재평가를 할 생각이 없었습니다. 또는 재평가는 이전에 개최 된 것과 다른 이유로 거절해야했습니다. (btw : 귀하의 의견의 요점을 추가했습니다)
— 중복 제거기

@Deduplicator 표현식 템플릿을 사용하면 지연 키워드 나 람다가 필요하지 않습니다.
— Sumant

역사적으로 제쳐두고, 원래 Algol 68 언어는 "프로 시저 링"강압 (deproceduring)을 가졌다는 점에 유의하십시오. 즉, 컨텍스트가 함수 타입이 아닌 결과 타입을 요구할 때 매개 변수가없는 함수를 암시 적으로 호출하는 것을 의미합니다. 이는 "모수없는 함수 반환 T"( 알골 68의 철자 " proc T") 값을 요구하는 위치에서 T 타입의 표현식 이 내재적으로 주어진 표현식 (암시 적 람다)을 리턴하는 함수 본문으로 변환 됨을 의미합니다 . 이 기능은 1973 년 언어 개정판에서 (절차와 달리) 제거되었습니다.
— Marc van Leeuwen

... C ++의 경우 비슷한 접근 방식은 &&"T를 리턴하는 함수에 대한 포인터"유형의 인수 하나와 T 유형의 인수 표현식을 내재적으로 람다 표현식으로 변환 할 수있는 추가 변환 규칙을 사용하는 연산자를 선언 할 수 있습니다 . 구문 레벨에서 수행해야하므로 이는 일반적인 변환이 아닙니다. 런타임시 T 유형 의 값 을 함수로 변환하는 것은 평가가 이미 수행되었으므로 사용되지 않습니다.
— Marc van Leeuwen

13

회고 적 합리화로 인해 주로

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

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


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

auto x = a && b || c;

여기서 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);

또는 아마도 더 명확하게

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, 즉 인수 값 확인 을 보여줍니다 .

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


나는 당신의 단락 대체물, 특히 삼항 대체물을 좋아합니다.
— iFreilicht

단락이 없습니다 -다음과 &&같은 추가 줄이 있어야합니다 if (!a) { return some_false_ish_T(); }-첫 번째 글 머리 기호 : 단락은 결과가 아닌 부울로 변환 가능한 매개 변수에 관한 것입니다.
— Arne Mertz

@ArneMertz : "Missing"에 대한 귀하의 의견은 의미가 없습니다. 무슨 일인지에 대한 의견, 예, 알고 있습니다. 로 변환 bool할 필요가 할 단락을.
— 건배와 hth. -Alf

@ Cheersandhth.- 실종에 대한 의견은 단락의 첫 번째 개정에 대한 ||것이었지만 단락은 하지 않았습니다 &&. 다른 의견은 첫 번째 글 머리표에서 "bool로 변환 가능한 결과 로 제한되어야 함"을 목표로했습니다. " bool 로 변환 가능한 매개 변수 로 제한됨 "imo를 읽어야 합니다.
— Arne Mertz

@ArneMertz : 알았어요. 버전을 다시 정했습니다. 죄송합니다. 편집 속도가 느립니다. 제한됨, 아니오 bool표현식에서 추가 연산자의 짧은 순환을 점검하기 위해 변환되어야하기 때문에 제한되어야하는 연산자 결과가 아닙니다. 마찬가지로의 결과 a && b는로 bool논리 OR의 짧은 순환을 확인하기 위해로 변환되어야 합니다 a && b || c.
— 건배와 hth. -Alf

5

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

마음에 오는 첫번째 것은 연산자 오버로딩은 사업자의 부울 버전 반면, 쓰기 기능에 단지 멋진 방법이다 ||과 &&물건 buitlin 있습니다. 컴파일러는 이들 단락의 자유를 가지며, 식 동안 것을 의미 x = y && znonboolean 함께 y와 z같은 함수의 호출을 야기한다 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||- 단지 손에 논리를 쓰는 것보다 적은 노력이 아니라는 결론에 도달합니다.


비용이 정말 높습니까? D 프로그래밍 언어를 사용하면 lazy인수로 제공된 표현식을 내재적으로 익명 함수로 변환하는 매개 변수를 선언 할 수 있습니다. 이것은 호출 된 함수에 해당 인수를 호출 할 것인지의 여부를 선택합니다. 따라서 언어에 이미 람다가 있으면 필요한 추가 구문이 매우 작습니다. "의사 코드": X 및 (A a, 게으른 B b) {if (cond (a)) {return short (a); } else {actual (a, b ()); }}
— BlackJack

@BlackJack 게으른 매개 변수는을 수락하여 구현할 수 있으며 std::function<B()>특정 오버 헤드가 발생합니다. 또는 인라인 할 의향이 있다면 그것을 만드십시오 template <class F> X and(A a, F&& f){ ... actual(a,F()) ...}. 그리고 "normal" B매개 변수로 오버로드 하여 호출자가 선택할 버전을 결정할 수 있습니다. lazy구문은 더 편리 할 수 있지만 특정 성능 트레이드 오프를 가지고있다.
— Arne Mertz

1
std::functionvs 의 문제점 중 하나는 lazy첫 번째를 여러 번 평가할 수 있다는 것입니다. 그대로 foo사용되는 게으른 매개 변수 foo+foo는 여전히 한 번만 평가됩니다.
— MSalters

"단락의 사용은 Y가 X에 적합하지 않은 경우로 제한 될 것입니다."아니요, 혼자서 X계산할 수있는 경우로 제한됩니다 Y. 매우 다릅니다. std::ostream& operator||(char* a, lazy char*b) {if (a) return std::cout<<a;return std::cout<<b;}. "전환"을 매우 일상적으로 사용하지 않는 한.
— Mooing Duck

1
@Sumant 그들은 할 수 있습니다. 그러나 단락 사용자 정의의 논리를 직접 작성할 수도 있습니다 operator&&. 가능하다면 문제는 아니지만 편리한 방법이 짧은 이유는 무엇입니까?
— Arne Mertz

5

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

5

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

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

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

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

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

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


4

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


의견과 @ 중복 제거기 답변에서 언급했듯이 추가 언어 기능으로 가능할 수 있습니다. 지금은 작동하지 않는다는 것을 알고 있습니다. 내 질문은 그런 기능이 없다는 이유가 무엇인지에 대한 질문이었습니다.
— iFreilicht

사용자 정의에 대한 추측을해야한다는 점을 고려하면 확실히 복잡한 기능 일 것입니다!
— nj-ath

무엇에 대한 : (<condition>)두 번째 인수가 평가되지되는 조건을 지정하는 연산자 선언 후?
— iFreilicht

@iFreilicht : 여전히 다른 단항 함수 본문이 필요합니다.
— MSalters

3

그러나 bool의 연산자에는 이러한 동작이 있습니다. 왜이 단일 유형으로 제한되어야합니까?

이 부분에 답하고 싶습니다. 그 이유는 내장 연산자 &&와 ||표현식이 오버로드 연산자처럼 함수로 구현되지 않았기 때문입니다.

특정 식에 대한 컴파일러의 이해에 내장 된 단락 논리를 갖는 것은 쉽습니다. 다른 내장 제어 흐름과 같습니다.

그러나 연산자 오버로드는 대신 특정 함수를 갖는 함수로 구현됩니다. 그 중 하나는 함수로 호출되기 전에 인수로 사용 된 모든 표현식을 평가한다는 것입니다. 분명히 다른 규칙을 정의 할 수는 있지만 더 큰 일입니다.


1
대가가의 질문에 주어졌다 궁금 여부 의 과부하 &&, ||및 ,허용해야 하는가? C ++에 오버로드가 함수 호출 이외의 다른 방식으로 작동 할 수있는 메커니즘이 없다는 사실은 해당 함수의 오버로드가 다른 작업을 수행 할 수없는 이유를 설명하지만 이러한 연산자가 처음에 오버로드 가능한 이유는 설명하지 않습니다. 실제 이유는 단순히 많은 생각없이 연산자 목록에 던져 졌기 때문이라고 생각합니다.
— supercat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.