There might be a way to achieve what you want with expression templates. Below a sketch of how to approach this (does not compile, lots of details missing, caveat lector). First you setup a class template to represent logical values and define some operators over them.
template<typename T, typename Type = Atomic<T> >
class Logical;
template<typename T, typename E1, typename E2>
Logical<T, OpOr<T, E1, E2> > operator||(Logical<T, E1> lhs, Logical<T, E2> rhs);
template<typename T, typename E1, typename E2>
Logical<T, OpAnd<T, E1, E2> > operator&&(Logical<T, E1> lhs, Logical<T, E2> rhs);
template<typename T, typename E1, typename E2>
Logical<T, OpEq<T, E1, E2> > operator==(Logical<T, E1> lhs, Logical<T, E2> rhs)
{ return OpEq<T, E1, E2>()(lhs, rhs); } // delegate to class template
Because function templates can't be partially specialized, you delegate your actual work to class templates.
// primary template
template<typename T, typename E1, typename E2> class OpEq;
// specialization for atomic comparisons
template<typename T>
class OpEq<T, Atomic<T>, Atomic<T> >
{
bool operator()(Atomic<T> lhs, Atomic<T> rhs)
{ return lhs == rhs; }
}
// apply distributive rule
template<typename T>
class OpEq<T, Atomic<T>, OpOr<T, Atomic<T>, Atomic<T> > >
{
bool operator()(Atomic<T> lhs, OpOr<T, Atomic<T>, Atomic<T> > rhs)
{ return (lhs == rhs.first()) && (lhs == rhs.second()); }
}
Obviously, there is a lot of heavy template machinery involved to get natural C++ syntax for what you want. But with a lot of effort and reading up you might eventually get something nice. (You'd have to define Atomic, OpAnd, OpOr, setup representations holding the first and second branches of subexpression etc. etc.)
However, even if you would succeed, you would get really weird semantics in your scheme. What you are proposing is require ==
to be left-distributive over ||
or &&
. I.e. to parse
X == (Y @OP Z)
as
(X == Y) @OP (X == Z)
with @OP
equal to &&
or ||
. I think it would be natural to require that ==
remains symmetric. This would require you also to impose right-distributivity of ==
over &&
and ||
. I.e. to parse
(X @OP Y) == Z
as
(X == Z) @OP (Y == Z)
However, if you combine the two with expression as (A @OP1 B) == (C @OP2 D)
, you get logical inconsistencies. E.g. the result is depending on the order in which you apply left-distribution and right-distribution.
Left-then-right:
(A @OP1 B) == (C @OP2 D)
((A @OP1 B) == C) @OP2 ((A @OP1 B) == D)
((A == C) @OP1 (B ==C)) @OP2 ((A == D) @OP1 (B == D))
Right-then-left:
(A @OP1 B) == (C @OP2 D)
(A == (C @OP2 D)) @OP1 (B == (C @OP2 D))
((A == C) @OP2 (A == D)) @OP1 ((B == C) @OP2 (B == D))
In both cases, the same 4 pairs of elements are being compared, but the way they are being propagated up the expression tree is subtly different. If @OP1
and @OP2
are the same, then you can flatten the entire tree and re-order the terms to get a unique result. , it works out OK if you use the same operators on both sides of the ==
, because both &&
and ||
are associative as well as commutative.
But for mixed operators the resulting expressions will in general be different.
UPDATE: as mentioned in the comments to this and other answers, you also loose certain properties of built-in types. First, the short-circuit rules, which are not obeyed by overloaded operators. For logical expressions not involving pointer dereferencing or other resource access (if(p && p->value())
or if(file && file.open())
etc.) this would not influence correctness but only efficiency. Otherwise be careful! Second, it was also mentioned that mixed evaluations of constants/expressions would go wrong. This has a simple (but verbose) fix: simply use std::integral_constant
(or boost::mpl::int_
) as a wrapper.
if (areAllEqual (X, Y, Z, A, B, C))
– Anchie