8

The following codes related to conditional expression:

typedef unsigned char uchar;
uchar data[100];
// assign something to array[] here
uchar *start = data;
uchar *end = data+100;
bool cond = f();  // f() could return true or false
uchar *itr = std::upper_bound(start, end, uchar(20), 
                cond? std::greater<uchar>() : std::less<uchar>());

got an error like this:

error: operands to ?: have different types 
‘std::greater<unsigned char>’ and ‘std::less<unsigned char>’

Is this a compiler bug? In my instinct, the two functor should have the same type.

Nathan Monteleone
  • 5,430
  • 29
  • 43
Robin Hsu
  • 4,164
  • 3
  • 20
  • 37

6 Answers6

5

std::greater and std::less are different types (just as different as std::string and std::unordered_map). They only provide same interface (operator()).

Conditional operator cond ? expr1 : expr2 requires, that expr1 and expr2 is of the same type (or one is implicitly convertible to another) and that is not your case.

You can use

char *itr;
if (cond)
    itr = std::upper_bound(start, end, uchar(20), std::greater<uchar>{});
else
    itr = std::upper_bound(start, end, uchar(20), std::less<uchar>{});

Or if you do not want to have two almost same lines, you can help yourself with generic lambda

auto upper_bound = [&](const auto& cmp){
    return std::upper_bound(start, end, uchar(20), cmp);
};
if (cond)
    itr = upper_bound(std::greater<uchar>{});
else
    itr = upper_bound(std::less<uchar>{});
Zereges
  • 5,139
  • 1
  • 25
  • 49
  • I would just use `auto&&`, because who cares if it is `const`. And `itr = cond?upper_bound(std::greater{}):upper_bound(std::less{});` would more closely match the OP's original code. – Yakk - Adam Nevraumont Sep 28 '16 at 17:46
3

Functors in C++ do not traditionally share a type. This makes it easy for the compiler to understand exactly which functor is being called, and causes a performance boost.

There is a mechanism in C++ called type erasure. This is where you forget what makes two types different, and only remembers what makes them the same.

The most common kind of type erasure in C++ is std::function. This function here type erases a stateless template that takes two T const& and returns a bool:

template<class T, template<class...>class Z>
std::function< bool(T const&, T const&) >
comp() { return Z<T>{}; }

we can then take your code and do this:

uchar *itr = std::upper_bound(start, end, uchar(20), 
            cond? comp<uchar, std::greater>() : comp<uchar, std::less>());

or alternatively:

template<class T>
using comp_sig = bool(T const&, T const&);

template<class T, template<class...>class Z>
comp_sig<T>* comp() {
  return [](T const& lhs, T const& rhs)->bool{
    return Z<T>{}(lhs, rhs);
  };
}

which gives you function pointers that call less or greater as required. This version might be slightly easier for compilers to optimize than the std::function one, as compilers are really good at optimizing constant function pointers.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
2

Functors are just regular types. std::greater<uchar> is a completely different type from std:less<uchar>. Just like how std::vector<uchar> is different from std::set<uchar>, even though their properties are similar!

You can achieve what you want to achieve if you move your ternary operator a bit though:

char *itr = cond ?
    std::upper_bound(start, end, uchar(20), std::greater<uchar>()) :
    std::upper_bound(start, end, uchar(20), std::less<uchar>());

It's slightly more duplication, but just as readable and will compile.

Karl Nicoll
  • 16,090
  • 3
  • 51
  • 65
0

I think it is not, as it is said it is different types. std::less is something like this :

template <class T>
struct Less
{
      bool operator()(const T & t1, const T & t2) ..
};
Renaud
  • 209
  • 2
  • 9
0

First, these two functors don't have the same type, even though they have same type of opetor(), which is, after template instantiation,

bool operator()(uchar lhs, uchar rhs)  

Similarly, just because vector<int> and deque<int> have same type of operator[] obviously doesn't mean they have the same type.

Back to your code, if you are able to use C++11, a simple & elegant solution is using lambda:

char *itr = std::upper_bound(start, end, uchar(20),  
            [cond](uchar lhs, uchar rhs) { return cond ? lhs > rhs : lhs < rhs); }

Otherwise, just writ a straightforward functor

class F{  
public:  
    explicit F(bool cond): m_cond(cond) {}
    bool operator(uchar lhs, uchar rhs) { return m_cond ? lhs > rhs : lhs < rhs); }  
private:
    bool m_cond;
};

Lastly, lambda is a pretty handy tool, I suggest you get to know more about it.
What is a lambda expression in C++11?
When to use functors over lambdas

Jason L.
  • 741
  • 6
  • 11
0

Convert both of them to lambdas (non-capture, non-generic). In that case, their types are still different, but they both convert naturally to a plain old function pointer bool (*) (uchar, uchar) and therefore the ?: works

uchar *itr = std::upper_bound(start, end, uchar(20),
                 cond?
                 ([](uchar x, uchar y){return std::greater<uchar>{}(x,y);})
                 :
                 ([](uchar x, uchar y){return std::less   <uchar>{}(x,y);})
             );

(This works for me on clang 3.5 and g++ 5.3, but I'm not sure how portable this is. Should I use unary + to force the conversion to the function pointer?

Aaron McDaid
  • 26,501
  • 9
  • 66
  • 88
  • 1
    This will probably break on MSVC because their lambdas have *four* implicit conversions to function pointers (one per calling convention), but I haven't tested it. – T.C. Sep 28 '16 at 18:44