Check for multiple values when using comparison operators [duplicate]
Asked Answered
O

4

13

I've always been under the impression that for any comparison statement, i.e. X == Y or X != Y is the format, and you chain statements together with && or ||.

Is there not some way to write X == (Y || Z) instead of X == Y || X == Z?

Edit: Since it has been established that this is not possible to do cleanly, how else could it be done?

Oligarchy answered 20/7, 2012 at 16:41 Comment(2)
You could probably have a variadic function taking the first argument as X and the rest as Y, Z, etc. Cancels out the operator syntax, but still works: if (areAllEqual (X, Y, Z, A, B, C))Anchie
Nice question, I was wondering the same thing..Wood
O
9
#include <algorithm>
#include <array>
#include <string>
#include <iostream>
#include <initializer_list>
 
template<class Type, class Next>
bool is_one_of(const Type& needle, const Next& next)
{return needle==next;}
template<class Type, class Next, class ... Rest>
bool is_one_of(const Type& needle, const Next& next, Rest... haystack)
{return needle==next || is_one_of(needle, haystack...);}
 
int main() {
    std::string X, Y;
    if (is_one_of(X, Y, "HI"))
        std::cout << "it is!";
    else
        std::cout << "it isn't!";
    return 0;
}

proof of compilation. Xeo also observes that std::any_of, std::all_of and std::none_of may have been useful, depending on your actual needs and desires.

Obvert answered 20/7, 2012 at 17:12 Comment(7)
std::initializer_list<E> should do the trick. ideone.com/UvwYV Note that this copies the operands, though.Irritable
This has quite some performance overhead, though - some allocations, creating a temporary, looping over an array.Lomond
@Xeo: I actully had that 99% coded, but it was failing so I was a hairsbreath from giving up when I saw your comment. When I looked at my code again, I saw it still had the template<class T, unsigned L> which was causing the failure. Thanks for the comment!Obvert
@nightcracker: Yes, it copies each of X and Y to the initializer list, and then destroys them at the end.Obvert
Also, this does not support mixing types.Lomond
@nightcracker: after some thought, I decided comparing a std::string against literals is a useful idea, and added mixing of types. It just means that function may not be found if you mix the types in the initializer list, instead of being types being simply promoted.Obvert
@nightcracker: I did a complete and total rewrite. No more iteration, copies, and allows mixed types.Obvert
B
3

There's no clean way to do what you ask in C++.

What trips many people up is that X == (Y || Z) may be a legal expression and the compiler will not complain. It will just be a bug. Each C++ statement must evaluate to true/false on its own and the operators just string them together. What you're suggesting would require some intrinsic list structure. Many languages have that (like Python), but C++ does not.

Bonhomie answered 20/7, 2012 at 16:50 Comment(1)
X == (Y || Z): I could see that if (x == (4 || 5)) which then evaluates to if (x == true), from my understanding. I can definitely see how that would be confusing to new programmers.Oligarchy
V
1

With operator overloading, you might be able to get the exact syntax that you want. But, as Adam points out, that could lead to excluding valid expressions.

Below is a template with operator overloading, a template function, and a macro to achieve a syntax similar to Mooing Duck's nicer solution, but without requiring C++11, and allowing the use of the || operator to denote the "haystack" collection.

template <typename T>
struct MultiOrComparable {
    mutable std::set<T> vals;
    const MultiOrComparable & operator || (T v) const {
        vals.insert(v); return *this;
    }
    bool operator == (T v) const { return vals.find(v) != vals.end(); }
};

template <typename T>
MultiOrComparable<T> MultiOrComparableStart (T) {
    return MultiOrComparable<T>();
}

#define IsOneOf(x, y) ((MultiOrComparableStart(x)||y) == x)

Then, the following program "works":

enum Foo { A, B, C, D };

int
main ()
{
    if (!IsOneOf(A, B || C || D)) {
        std::cout << "!=" << std::endl;
    }
    if (IsOneOf('a', 'x' || 'y' || 'z' || 'a')) {
        std::cout << "==" << std::endl;
    }
}
Vestry answered 20/7, 2012 at 17:11 Comment(0)
F
1

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.

Fluttery answered 20/7, 2012 at 20:4 Comment(3)
One very big problem with overloading the logical operators is that you lose the very important property of short-circuiting, which you don't mention at all.Irritable
@Irritable I didn't mention that because it was a) so obvious and b) because I thought that the OP was willing to pay that price. Still, in e.g. a expression template library for symbolic logic computations, the chances of making pointers errors would be very small, wouldn't you agree?Fluttery
@Irritable Anyway, I updated the answer with your comment. Thanks again.Fluttery

© 2022 - 2024 — McMap. All rights reserved.