Implicit C ++ type conversion in argument lists

I am confused about how implicit type conversion works in relation to C ++ argument lists. In particular, I have a bunch of functions called inRange (x, start, end) that return bool depending on whether x is between the beginning and the end.

[In this description, inRange is just syntactic sugar for (x> start & x <end) - which is still good when x is a long line or an expensive function, but in real code there are additional arguments for open / closed processing borders.]

I was vague about the types above. In particular, there are various implementations for comparing integer and floating point, which means that the templates are not very appropriate, since there is no C ++ linguistic group that distinguishes int / long / unsigned / size_t, etc. From float / double, etc. Therefore, I tried to use a type system by defining two versions of inRange with wide enough int / float types:

inline bool inRange(long x, long start, long end) inline bool inRange(double x, double start, double end) 

This will not catch β€œlong long” or similar, but our code uses no more than two pairs and lengths. This way, it looked pretty safe: I was hoping inRange (int, long, long), etc. Implicitly raise int to the end, and everything will be fine. However, in cases where letter doubles are written sloppily for comparison with a floating point (which I want to allow), for example. inRange (mydouble, 10, 20), I also had to add a bunch of explicit tricks to get rid of compiler warnings and make sure that floating point comparisons were used:

 inline bool inRange(double value, long low, long high) { return inRange(value, (double)low, (double)high); } inline bool inRange(double value, double low, long high) { return inRange(value, low, (double)high, lowbound, highbound); } ... 

Not so nice - I was hoping that converting long to double would be automatic / implicit - but everything is fine. But the following discovery really messed me up: my compiler met inRange with three ints (not longs) as arguments and said:

 call of overloaded 'inRange(int&, int&, int&)' is ambiguous 

and then a list of all the inRange functions defined so far! So, C ++ has no preferences (int, int, int) for arg arguments that should be allowed (long, long, long), and not (double, double, double)? Really?

Any help to get me out of this hole would be greatly appreciated ... I would never have thought that something was so simple that only primitive types could be so difficult to solve. Creating a complete set of ~ 1000 function signatures with three arguments with all possible combinations of a numeric type is not the answer I hope for!

+4
source share
2 answers

The templates are the basics, you just need SFINAE.

 #include <limits> #include <utility> template <typename T> struct is_integral { static bool const value = std::numeric_limits<T>::is_integer; }; template <typename Integral, typename T> typename std::enable_if<is_integral<Integral>::value, bool>::type inRange(Integral x, T start, T end) { return x >= static_cast<Integral>(start) and x <= static_cast<Integral>(end); } template <typename Real, typename T> typename std::enable_if<not is_integral<Real>::value, bool>::type inRange(Real x, T start, T end) { return x >= static_cast<Real>(start) and x <= static_cast<Real>(end); } 

In theory, we could be even softer and just let start and end have different types. If we want.

EDIT : Modified to upgrade to the real version as soon as there is one real version with built-in health check.

 #include <limits> #include <utility> #include <iostream> template <typename T> struct is_integral { static bool const value = std::numeric_limits<T>::is_integer; }; template <typename T> struct is_real { static bool const value = not is_integral<T>::value; }; template <typename T, typename L, typename R> struct are_all_integral { static bool const value = is_integral<T>::value and is_integral<L>::value and is_integral<R>::value; }; template <typename T, typename L, typename R> struct is_any_real { static bool const value = is_real<T>::value or is_real<L>::value or is_real<R>::value; }; template <typename T, typename L, typename R> typename std::enable_if<are_all_integral<T, L, R>::value, bool>::type inRange(T x, L start, R end) { typedef typename std::common_type<T, L, R>::type common; std::cout << " inRange(" << x << ", " << start << ", " << end << ") -> Integral\n"; return static_cast<common>(x) >= static_cast<common>(start) and static_cast<common>(x) <= static_cast<common>(end); } template <typename T, typename L, typename R> typename std::enable_if<is_any_real<T, L, R>::value, bool>::type inRange(T x, L start, R end) { typedef typename std::common_type<T, L, R>::type common; std::cout << " inRange(" << x << ", " << start << ", " << end << ") -> Real\n"; return static_cast<common>(x) >= static_cast<common>(start) and static_cast<common>(x) <= static_cast<common>(end); } int main() { std::cout << "Pure cases\n"; inRange(1, 2, 3); inRange(1.5, 2.5, 3.5); std::cout << "Mixed int/unsigned\n"; inRange(1u, 2, 3); inRange(1, 2u, 3); inRange(1, 2, 3u); std::cout << "Mixed float/double\n"; inRange(1.5f, 2.5, 3.5); inRange(1.5, 2.5f, 3.5); inRange(1.5, 2.5, 3.5f); std::cout << "Mixed int/double\n"; inRange(1.5, 2, 3); inRange(1, 2.5, 3); inRange(1, 2, 3.5); std::cout << "Mixed int/double, with more doubles\n"; inRange(1.5, 2.5, 3); inRange(1.5, 2, 3.5); inRange(1, 2.5, 3.5); } 

Run ideone :

 Pure cases inRange(1, 2, 3) -> Integral inRange(1.5, 2.5, 3.5) -> Real Mixed int/unsigned inRange(1, 2, 3) -> Integral inRange(1, 2, 3) -> Integral inRange(1, 2, 3) -> Integral Mixed float/double inRange(1.5, 2.5, 3.5) -> Real inRange(1.5, 2.5, 3.5) -> Real inRange(1.5, 2.5, 3.5) -> Real Mixed int/double inRange(1.5, 2, 3) -> Real inRange(1, 2.5, 3) -> Real inRange(1, 2, 3.5) -> Real Mixed int/double, with more doubles inRange(1.5, 2.5, 3) -> Real inRange(1.5, 2, 3.5) -> Real inRange(1, 2.5, 3.5) -> Real 
+2
source

(Lazy approach :) uses a function template - and let the compiler worry about it.

 template <typename T1> inline bool inRange(T1 x, T1 start, T1 end) { // do stuff } 

This means that you can pass any object that supports operator< ... and overloading for certain types where you want to do something else (e.g. std::string , const char* , etc.)

0
source

Source: https://habr.com/ru/post/1396642/


All Articles