Overloading short-circuit

Pages: 1234
Someone, I can't remember who, mentioned the other day that it wasn't possible to overload && or || and still have short-circuit semantics. This is true at the low level, but it's possible to emulate those semantics from high level with even more operator overloading, by using the compiler as a static expression parser and evaluating the expression at run time. The only drawback is that you have to choose only two of these: syntactical niceness, efficiency, or simplicity.
This is the nice simple version:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#include <iostream>
#include <vector>

class IntExpression;

class Int{
	int data;
	friend class IntExpression;
public:
	Int():data(0){}
	Int(int a):data(a){}
	Int(const Int &a):data(a.data){}
	const Int &operator=(int a){
		std::cout <<"Int::operator=()\n";
		this->data=a;
		return *this;
	}
	int to_int(){
		return this->data;
	}
	template <typename T> IntExpression operator!();
#define Int_OVERLOAD_OPERATOR(op) template <typename T> IntExpression operator op(const T &);
	Int_OVERLOAD_OPERATOR(==);
	Int_OVERLOAD_OPERATOR(!=);
	Int_OVERLOAD_OPERATOR(>);
	Int_OVERLOAD_OPERATOR(>=);
	Int_OVERLOAD_OPERATOR(<=);
	Int_OVERLOAD_OPERATOR(<);
	Int_OVERLOAD_OPERATOR(&&);
	Int_OVERLOAD_OPERATOR(||);
};

class IntExpression{
public:
	enum Operation{
		NOP=0,
		NOT,
		EQ,
		NEQ,
		GT,
		GTEQ,
		LTEQ,
		LT,
		AND,
		OR
	};
private:
	Operation op;
	IntExpression *operands[2];
	Int i;
public:
	IntExpression(const Int &i):i(i){
		this->op=NOP;
		this->operands[0]=0;
		this->operands[1]=0;
	}
	IntExpression(const int &i):i(i){
		this->op=NOP;
		this->operands[0]=0;
		this->operands[1]=0;
	}
	IntExpression(Operation op,const IntExpression &operand){
		this->op=op;
		this->operands[0]=new IntExpression(operand);
		this->operands[1]=0;
	}
	IntExpression(Operation op,const IntExpression &a,const IntExpression &b){
		this->op=op;
		this->operands[0]=new IntExpression(a);
		this->operands[1]=new IntExpression(b);
	}
	IntExpression(const IntExpression &e):op(e.op),i(e.i){
		std::cout <<"IntExpression::IntExpression(const IntExpression &e)\n";
		for (int a=0;a<2;a++)
			this->operands[a]=(e.operands[a])?new IntExpression(*e.operands[a]):0;
	}
	~IntExpression(){
		for (int a=0;a<2;a++)
			delete this->operands[a];
	}
#define IntExpression_OVERLOAD_OPERATOR(op,val)\
	IntExpression operator op(int a){ return IntExpression((val),*this,a); }\
	IntExpression operator op(Int a){ return IntExpression((val),*this,a); }\
	IntExpression operator op(IntExpression a){ return IntExpression((val),*this,a); }
	IntExpression_OVERLOAD_OPERATOR(==,EQ)
	IntExpression_OVERLOAD_OPERATOR(!=,NEQ)
	IntExpression_OVERLOAD_OPERATOR(>,GT)
	IntExpression_OVERLOAD_OPERATOR(>=,GTEQ)
	IntExpression_OVERLOAD_OPERATOR(<=,LTEQ)
	IntExpression_OVERLOAD_OPERATOR(<,LT)
	IntExpression_OVERLOAD_OPERATOR(&&,AND)
	IntExpression_OVERLOAD_OPERATOR(||,OR)
	int eval(){
		int r;
		switch (this->op){
			case NOP:
				r=this->i.to_int();
				std::cout <<"eval::int("<<r<<")\n";
				break;
			case NOT:
				r=!this->operands[0]->eval();
				std::cout <<"eval::operator!()\n";
				break;
			case EQ:
				r=this->operands[0]->eval()==this->operands[1]->eval();
				std::cout <<"eval::operator==()\n";
				break;
			case NEQ:
				r=this->operands[0]->eval()!=this->operands[1]->eval();
				std::cout <<"eval::operator!=()\n";
				break;
			case GT:
				r=this->operands[0]->eval()>this->operands[1]->eval();
				std::cout <<"eval::operator>()\n";
				break;
			case GTEQ:
				r=this->operands[0]->eval()>=this->operands[1]->eval();
				std::cout <<"eval::operator>=()\n";
				break;
			case LTEQ:
				r=this->operands[0]->eval()<=this->operands[1]->eval();
				std::cout <<"eval::operator<=()\n";
				break;
			case LT:
				//VERY LATE EDIT:
				//r=this->operands[0]->eval()>this->operands[1]->eval();
				r=this->operands[0]->eval()<this->operands[1]->eval();
				std::cout <<"eval::operator<()\n";
				break;
			case AND:
				r=this->operands[0]->eval() && this->operands[1]->eval();
				std::cout <<"eval::operator&&()\n";
				break;
			case OR:
				r=this->operands[0]->eval() || this->operands[1]->eval();
				std::cout <<"eval::operator||()\n";
				break;
		}
		return r;
	}
	operator bool(){
		return this->eval();
	}
};

template <typename T>
IntExpression Int::operator!(){
	std::cout <<"Int::operator!()\n";
	return IntExpression(IntExpression::NOT,*this);
}

#define Int_DEFINE_OVERLOADED_OPERATOR(op,val)           \
template <typename T>                                    \
IntExpression Int::operator op(const T &a){              \
	return IntExpression(IntExpression::val,*this,a);\
}
Int_DEFINE_OVERLOADED_OPERATOR(==,EQ)
Int_DEFINE_OVERLOADED_OPERATOR(!=,NEQ)
Int_DEFINE_OVERLOADED_OPERATOR(>=,GTEQ)
Int_DEFINE_OVERLOADED_OPERATOR(>,GT)
Int_DEFINE_OVERLOADED_OPERATOR(<,LT)
Int_DEFINE_OVERLOADED_OPERATOR(<=,LTEQ)
Int_DEFINE_OVERLOADED_OPERATOR(&&,AND)
Int_DEFINE_OVERLOADED_OPERATOR(||,OR)

int main(){
	Int a=13;
	if (a<9 || a>12)
		std::cout <<"a>9 || a<12\n";
	return 0;
}

This can be used with nearly no modifications for any class that can be losslessly converted to an int, and with a few modifications for a class that can't, such as, say, a bignum. There are optimizations that can be made to avoid a lot of unnecessary copies, but this was just a demonstration. More complicated semantics, such as lazy evaluation, can also be implemented, I think.
Last edited on
closed account (EzwRko23)
Nice explanation why better **not** boost C++.
Here is something similar implemented in Scala:

1
2
3
case class MyBool(value: Int) {
  def && (other: => MyBool) = if (value == 0) false else (other.value != 0)
}
Last edited on
Nice explanation why better **not** boost C++.
Mmh... No, I don't see why not. And this is more about boosting Int, not C++.
Personally, I'm not a fan of short circuiting even with basic types. I avoid || and && in situations where failure to short circuit would cause problems.

But a neat trick nonetheless, I suppose. Even if it is a little type-sloppy.
I didn't know what you were talking about, so I searched Wikipedia...
Wikipedia wrote:
Short-circuit evaluation, minimal evaluation, or McCarthy evaluation denotes the semantics of some boolean operators in some programming languages in which the second argument is only executed or evaluated if the first argument does not suffice to determine the value of the expression

I thought C did this by default? Is it different in C++? In The C Programming Language it says that 'evaluation is assured to stop as soon as a truth value is found', meaning that in
1
2
int A = 1,
    B = 0;

the expression, (A || B) would evaluate to true, and B would never be evaluated.
Last edited on
In C++ this does not work when operator || is overloaded.
Oh, ok.
Think of it as just another function call: operator||( lhs, rhs ). If a function takes two parameters, and the arguments given are themselves function calls like f3( f1(), f2() ), both f1 and f2 will run before f3.

Interesting thread, btw.
Last edited on
closed account (EzwRko23)
Don't you think that writing over 150 LOC to just simulate (imperfectly) something not available by default in the language is a good thing?

Don't you think that writing over 150 LOC to just simulate (imperfectly) something not available by default in the language is a good thing?
Yes. It is, isn't it?
Leaving your misuse of idiomatic structures aside, the point of any class is implementing something that's not available in the language. Now, we can argue all day about whether or not it should be part of it, and the merits and demerits of it being a part or not, but my point is that it can be done. Figuring out when to do it is left as an exercise for the reader.
closed account (EzwRko23)

Leaving your misuse of idiomatic structures aside


What misuse? Where? The code I posted in this thread is perfectly idiomatic. If you are bothered by overloading the && operator for a custom purpose - no problem, I can overload &&& and ||| operators to avoid confusion with the builtin && and ||. Can you do this with your macro hacks?

1
2
3
4
case class MyBool(value: Int) {
  def &&& (other: => MyBool) = if (value == 0) false else (other.value != 0)
  def ||| (other: => MyBool) = if (value == 1) true else (other.value != 0)
}




The point of any class is implementing something that's not available in the language.


True, but sometimes it is not worth it. Your code is not idiomatic C++, it uses macros and it forces the extenders of the class to use macros instead of ordinary methods, it requires touching a few places to add a single operation (it violates OnceAndOnlyOnce and DRY principles), it is long and slow as molasses. It has probably similar performance to that of interpreted Java in 1995.
The short-circuit behaviour of your code also breaks when you start using function calls like that:

1
2
3
4
Int a=13;
if (a < 10 && costlyConditionIsTrue()) {   // the costlyConditionIsTrue function **will be** called
  ....
}


A nice academic example of what can be hacked together with macros, but not to be used in production code.
Last edited on
closed account (z05DSL3A)
I think that helios may have been refering to the sentence, "Don't you think that writing over 150 LOC to just simulate (imperfectly) something not available by default in the language is a good thing?", when he talked about "misuse of idiomatic structures". I have read it several times and I don't think what you are asking is what you mean to ask.

To me this reads as: (paraphrasing)
Do you not think it is a good thing to write code to implement things that are not present by default?
it uses macros
Only because I was being lazy.

it is [...] slow as molasses.
Like I said, that's the nice and simple version. There are optimizations that can be performed.

(a < 10 && costlyConditionIsTrue())
1
2
3
4
5
6
7
8
9
bool costlyConditionIsTrue(){
	return 1;
}

int main(){
	Int a=13;
	if (a<10 || costlyConditionIsTrue());
	return 0;
}
test.cpp: In function `int main()':
test.cpp:170: error: ISO C++ says that these are ambiguous, even though the worst conversion for the first is better than the worst conversion for the second:
test.cpp:92: note: candidate 1: IntExpression IntExpression::operator||(int)
test.cpp:170: note: candidate 2: operator||(bool, bool) <built-in>
closed account (EzwRko23)
I meant:

1
2
3
4
5
6
7
8
9
10
Int costlyConditionIsTrue(){
  cout << "Executed";	
  return 1;
}

int main(){
	Int a=13;
	if (a<10 || costlyConditionIsTrue());
	return 0;
}


Last edited on
You return an Int from a function that ends with "IsTrue"?

Anyway, here: http://pastebin.com/T24PdgTt
This only allows Ints or ints to be used in expressions. Temporaries will be rejected by the compiler. You can force the compiler to accept the value by making the conversion to IntExpression explicitly, but in doing so you accept losing special semantics. I suppose I could figure out a way to make function calls, but I don't have the time right now.
Last edited on
closed account (EzwRko23)

You return an Int from a function that ends with "IsTrue"?


Only because you named it Int, though it can work as a boolean because it accepts && and || operators. Anyway, you should probably have 2 separate types for Bool and Int, but that doesn't help the problem of function calls go away.

So, you ended up with a leaky abstraction - it works in simple cases, but does not work as expected in **all** cases. This is far worse than lack of such abstraction at all. C++ is plagued by features that work well in isolation, but break or cause severe problems when used together with other features.
No. I can make it work with functors, I just don't have the time to do it. Maybe someone else who knows what I'm thinking wants to do it. Hint: limit the design to allow passing n Ints and returning an Int.
closed account (EzwRko23)
It would require programmer to create appropriate functors on the client side then. In C++, there is no way to support lazy argument evaluation so that it is fully transparent to the user (works **exactly** as in the built-in && || operators). The reason for that is you have no implicit lambda expression construction, so even when C++0x with support for lambdas finally comes out, you won't be able to do it.

Last edited on
It would require programmer to create appropriate functors on the client side then.
It's the same for any library. You can't just pass a function pointer when the library expects a float and expect it to work. You have to follow the proper protocol.

In C++, there is no way to support lazy argument evaluation so that it is fully transparent to the user
It's never possible to fully transparently implement syntactical features that aren't a part of a language. Well, unless you can modify the parsing algorithm through code, but that would be just insane.

PS: You know, you're getting quite good at being an insufferably annoying prick.
Last edited on
closed account (EzwRko23)
Well, it was you who claimed it can be done. Now you say it cannot be done.


You can't just pass a function pointer when the library expects a float and expect it to work.


If C++ was able to express appropriate implicit conversion, it would work. But implicit conversions in C++ are quite limited: you cannot provide a conversion from an anonymous block of code to a functor object.


It's never possible to fully transparently implement syntactical features that aren't a part of a language


But if C++ supports the && operator that does short-circuit and also supports operator overloading, you should be able to overload the && operator without changing its semantics or you should not be able to overload it at all. This operator is somewhere inbetween. It is not a keyword / syntax element (you usually cannot add keywords to the language or redefine them) but also not a library feature (because you cannot write another operator/method that behaves exactly like that). That's why it is so ugly.
Last edited on
Pages: 1234