Check for multiple values when using comparison op

2020-08-10 19:42发布

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?

4条回答
Luminary・发光体
2楼-- · 2020-08-10 19:46

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.

查看更多
Luminary・发光体
3楼-- · 2020-08-10 19:48

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;
    }
}
查看更多
爱情/是我丢掉的垃圾
4楼-- · 2020-08-10 20:01
#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.

查看更多
干净又极端
5楼-- · 2020-08-10 20:13

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.

查看更多
登录 后发表回答