Then, in a flash of inspiration, I realised that templates do in fact save the day if you decouple the type deductions. As long as your two inputs are the same type - or sufficiently compatible that '>' is defined and you can return one as if it was the other - you can replace the triply-defined template with this single one:
Ah – and here is where my original wild guess about std::enable_if comes in, because if you wanted to enforce that those two types really are the same up to references, you can make it into
template<typename T, typename U,
typename = typename std::enable_if<std::is_same<
typename std::remove_reference<T>::type,
typename std::remove_reference<U>::type>::value>::type>
T&& rvmax(T&& u, U&& v) { return std::forward<T>(u>v ? u : v); } which looks ugly – not much chance of avoiding that in C++ templates – but at least semantically it's directly specifying exactly what I asked for.
But even so, I'm afraid, there's still something wrong. With or without my enable_if clause, I can't do this:
char g(char x) { return rvmax(x, 1); } because the integer literal has type int , which isn't compatible with the char .
You need to be careful with mixing and matching rvalues and lvalues in this way. In the general case, what do you want to get out - an rvalue or an lvalue?
I think jack more or less nailed the requirements, when he observed that the naïve and dangerous macro implementation of max , for all its failings in other areas, actually does everything I want on the rvalue/lvalue axis. What I want is exactly the same semantics w.r.t. the two input values that the C++ version of the ternary operator would give: return a reference if possible, and if not (either because an input isn't an lvalue to begin with or because it stopped being one due to an implicit type conversion), fall back to returning by value. You might call it "opportunistic lvalue semantics".
Put another way: any invocation of max that would have worked before anyone even started thinking about lvalues should still compile and run successfully somehow, but now, the ones that can return an lvalue should.
In other words, what I really wanted was exactly the thing I tried for in the original post: simply provide both the by-value and the by-reference templates, and have the compiler pick the by-reference one whenever it can and fall back to the other one if it can't. But the problem was that if both templates are valid, the compiler complained rather than picking the one I wanted.
Aha – but now that I've conditioned the reference version with an enable_if , perhaps now I can achieve that by putting the explicit by-value version of the template back in, and conditioning it in the same way on exactly the inverse condition? Then the two templates can never both be valid, and I should get what I want in both cases.
In other words, if you use the above template with an enable_if , and adding this template alongside it, that seems to do what I wanted in every case I've so far checked:
template<typename T, typename U,
typename = typename std::enable_if<!std::is_same<
typename std::remove_reference<T>::type,
typename std::remove_reference<U>::type>::value>::type>
auto rvmax(T u, U v) { return u > v ? u : v; } although I did need to adjust it by changing the return type to auto , so as to return the same type that the heterogeneous ?: would have chosen given the same pair of input types.
I think the biggest remaining risk is that there might still be some class of input type pairs for which the wrong one of these templates will end up being selected. I can't think of one right now, but it seems very plausible that the next comment will point one out and I'll slap my forehead. But hopefully whatever it is can be worked around by adding further clauses to the two enable_if expressions.
(And even if we've finally got it right now, I don't think this discussion would really sell C++ to someone who wasn't already committed to it for a given project! :-) |