Freigeben über


Templatized Min/Max Solved!

I had some time to think about the overall problem, and had originally thought of a functional approach, like so:

template <typename R, typename T, typename U> R Max(T t, U u);

This has all the information we need to check for truncation on return, but it requires that the programmer know what the return type needs to be. This seems to be a modest constraint, but in practice, people foul this up, and it's subject to problems where we'd like to just pass the result directly into a function, and what if the function changes signature?

So let's drop back and look at the requirements:

  1. The function must deal correctly with mixed types (e.g., (DWORD)2 > (int)-1)
  2. We must be able to correctly determine if the return can be properly assigned.
  3. Programmer should get to choose what happens if the return cannot be properly assigned (maybe returning the max for that type makes sense?)
  4. We should not evaluate anything more than once.

The first constraint is easy. There are exactly 5 cases for correctly doing a comparison – no cast needed, cast to int, cast to int64, and 2 hard cases involving unsigned __int64 mixed with signed ints that needs a little special logic. I have this code written in SafeInt already, so let's re-use that. Here's the start:

template <typename T, typename U>

class SafeMax

{

public:

    SafeMax(T t, U u) : m_t(t), m_u(u){}

private:

    SafeInt<T> m_t;

    SafeInt<U> m_u;

};

We now have a class where forcing the assignment to a SafeInt keeps you from fouling things up because say classes Foo and Bar both implement the < operator, and you can feed both of these to a typical max macro, and then the results may or may not compile, and likely don't make sense. Putting things into a SafeInt means we'll only work with regular integral types. We might want to allow for some overloads in case a SafeInt gets passed in to start with. This also has the very nice side-effect of meeting requirement 4, which is that we don't evaluate things more than once.

Since we're now working with SafeInt internally, it will go ahead and not only do the comparison correctly, it will pick just the right function to do the job because of partial template specialization, so this deals with the first requirement that our max function not tell us that -1 > 2.

Meeting requirement 2 had me baffled for much the same reasons Andrei Alexandrescu gave in his article about this problem. However, if we make a simplifying assumption that we only need to deal with integral types, which solve at least 99% of our problem, life gets much simpler. In SafeInt, one of the best features is that it will correctly cast itself to whatever a function requires. This is how we can change code flow according to return type! There are 11 of these (remember that char, signed char and unsigned char are 3 different types, though effectively are only two, and while long and int are really the same thing on most platforms, as far as the compiler is concerned, they're both different types), and the implementation is simple – just do this:

operator char() const { return Max<char>(); }

The implementation of Max() is much the same as what we typically use, except that I added logic to check to see if the value being returned would actually fit in the type requested. In my current implementation, I'm going to allow for a #define to change whether it will just return the largest int of that type that actually fits (after firing an assert), or whether it throws an exception like the rest of SafeInt.

Overall, it's astonishingly hard to make a proper C++ rendition of something as simple as min and max, but by leveraging partial template specialization so that our common cases are efficient and by realizing that if you force a class to be unboxed only through the casting operators, you then get to run different code on the basis of the return type. The really nice thing about this is the ease of use – it will appear to the code very much like the macro we're replacing:

int foo = SafeMax( a, b );

We don't have to worry about the types of a and b, nor do we get multiple evaluation warnings, and we don't get signed-unsigned mismatch warnings, either. Even nicer is that say we have some function Bar, which takes some int type as a parameter, we can just do this:

Bar( SafeMax(a, b) );

If you're in fixing code, there's no immediate need to go sort out what type Bar was expecting. Obviously, you'd like to at least be aware if we're taking the max of an int, an unsigned int and assigning the result to a short, but if there are constraints on the inputs, maybe there isn't a problem.

Comments