짧은 단락의 운영자의 행동 &&
과는 ||
프로그래머를위한 놀라운 도구입니다.
그러나 오버로드 될 때 왜이 동작이 손실됩니까? 연산자는 함수의 구문 설탕 일 뿐이지 만 연산자 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 : alternative
과 while(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
, 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
, 즉 인수 값 확인 을 보여줍니다 .
컴파일러가 쉽게 동일한 작업을 수행하고 추가로 최적화 할 수 있기 때문에 이는 가능한 구현 방법이며 불가능성에 대한 주장은 일반적으로 불가능 성 주장과 동일한 범주, 즉 일반적으로 볼록으로 표시해야합니다.
답변
tl; dr : 높은 비용 (특별 구문 필요)과 비교할 때 수요가 매우 적기 때문에 (누가이 기능을 사용할 것인가?) 노력할 가치가 없습니다.
마음에 오는 첫번째 것은 연산자 오버로딩은 사업자의 부울 버전 반면, 쓰기 기능에 단지 멋진 방법이다 ||
과 &&
물건 buitlin 있습니다. 컴파일러는 이들 단락의 자유를 가지며, 식 동안 것을 의미 x = y && z
nonboolean 함께 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||
– 단지 손에 논리를 쓰는 것보다 적은 노력이 아니라는 결론에 도달합니다.
답변
람다는 게으름을 일으키는 유일한 방법은 아닙니다. 게으른 평가는 C ++의 Expression Templates 를 사용하여 비교적 간단 합니다. 키워드가 필요 없으며 lazy
C ++ 98에서 구현할 수 있습니다. 식 트리는 이미 위에서 언급했습니다. 식 템플릿은 가난하지만 영리한 사람의 식 나무입니다. 트릭은 표현식을 재귀 적으로 중첩 된 Expr
템플릿 인스턴스화 트리로 변환하는 것 입니다. 나무는 시공 후 별도로 평가됩니다.
다음 코드 는 무료 기능을 제공 하고 로 변환 할 수있는 한 단락 &&
및 ||
연산자 클래스 S
를 구현 합니다 . 코드는 C ++ 14에 있지만 아이디어는 C ++ 98에도 적용됩니다. 라이브 예를 참조하십시오 .logical_and
logical_or
bool
#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;
}
답변
논리 연산자는 연관된 진리표의 평가에서 “최적화”이기 때문에 단락이 허용됩니다. 논리 자체 의 기능 이며이 논리가 정의됩니다.
과부하 이유 실제로 거기
&&
와||
쇼트을은?
사용자 정의 오버로드 된 논리 연산자는 이러한 진리표의 논리를 따를 의무 가 없습니다 .
그러나 오버로드 될 때 왜이 동작이 손실됩니까?
따라서 전체 기능을 정상대로 평가해야합니다. 컴파일러는이를 일반 과부하 연산자 (또는 함수)로 취급해야하며 다른 함수와 마찬가지로 최적화를 적용 할 수 있습니다.
사람들은 다양한 이유로 논리 연산자를 과부하시킵니다. 예를 들어; 그것들은 사람들이 익숙한 “정상적인”논리적 인 것이 아닌 특정 영역에서 특정한 의미를 가질 수 있습니다.