I would like to understand better about overloading &&
and ||
operators and the loss of its short-circuit behaviour.
This question stems from my attempt at implementing a lazy-evaluated data value holder of template type T
. Consider such a struct, e.g.
template <typename T>
struct Value {
// value holder & accessor
T m_val;
virtual T getValue() {return m_val}; // trivial, but potentially complicated computation in derived classes
// will come into play below
template <typename U, typename V>
struct LogicalAnd;
};
And the value of T
that any instance holds is accessed by getValue()
. The types I am interested in are primarily arithmetic data types, and the use case for this class will be that its value can be accessed by getValue()
, but also that I could define an arbitrary derived class MyValue<T>
later down the road that could perform some heavy computation to store & return different values throughout the runtime of the program.
Given that, I'd like to be able to "book" operations, e.g. &&
, between two instances to reflect their lazily-updated values. A straightforward implementation of operator overloading does not achieve this, since it will return the basic data type value as evaluated at that line:
// overload && operator
template <typename U, typename V>
bool operator&&(Value<U>& a, Value<V>& b) {
return (a && b);
} // returns a basic data type bool evaluated at above line. X
So, I instead can write a proxy class representing the operation that I want and overload the operator such that it returns an instance of the proxy.
// addition of two existing Value
template <typename T>
template <typename U, V>
struct Value<T>::LogicalAnd {
virtual T getValue() override {
return m_valA->getValue() && m_valB->getValue();
}
Value<U>* m_valA;
Value<V>* m_valB;
};
// overload && operator
template <typename U, typename V>
auto operator&&(Value<U>& a, Value<V>& b) {
return typename Value<decltype(a.m_val && b.m_val)>::template LogicalAnd<U,V>{&a, &b};
} // returns a Value<X>::LogicalAnd<U,V>, X is most likely bool.
Then, I can get the lazy evaluation I want by calling getValue()
of this proxy instance:
Value<double> a{1.0};
Value<int> b{0.0};
c = a&&b; // c is Value<bool>::LogicalAnd<double,int>
c.getValue(); // = false
b.m_val = 1.0; // update the value of b
c.getValue(); // = true!!!
The question is regards to whether this would still be considered losing short-circuit behaviour of the operation. As I see it, it should be at least effectively preserved, as the built-in &&
operator is being used by LogicalAnd
implementation and heavy computation is done on-the-fly by the operands' getValue()
methods. In other words, whenever I call c.getValue()
, if a.getValue()
is already false
, then b.getValue()
would not be called. Would a use case such as this warrant a "good-enough" practice for overloading these operators?