Why c++ compiler gives error when using `std::max()` function for two different type of numeric variable

Viewed 1769

Is there any reason that c++ compiler gives error when using two different numeric variable types in std::max() function? (e.g. int and long).

I mean something like: "Sometimes we have this problem when using std::max() function for two different numeric variable types, so the compiler gives error to prevent this problem".

3 Answers

The compiler produces an error because it cannot perform type deduction for the template argument of std::max. This is how std::max template is declared: the same type (template parameter) is used for both arguments. If the arguments have different types, the deduction becomes ambiguous.

If you work around the deduction ambiguity by supplying the template argument explicitly, you will be able to use different types as std::max arguments

std::max(1, 2.0); // Error
std::max<double>(1, 2.0); // OK

The reason why std::max insists on using a common type for its arguments (instead of using two independent types) is described in @bolov's answer: the function actually wants to return a reference to the maximum value.

std::max returns a reference to the argument that has the maximum value. The main reason it is this way is because it is a generic function and as such it can be used with types expensive to copy. Also you might actually need just a reference to the object, instead a copy of it.

And because it returns a reference to a argument, all arguments must be of the same type.

The direct answer to the question is that it's because std::min and std::max take only one template parameter that defines the types of both arguments. If/when you try to pass arguments of different types, the compiler can't decide which of those two types to use for the template argument, so the code is ambiguous. As originally defined in C++98, std::min and std::max had signatures like these (C++03, §[lib.alg.min.max]):

template<class T> const T& min(const T& a, const T& b);

template<class T, class Compare>
const T& min(const T& a, const T& b, Compare comp);

template<class T> const T& max(const T& a, const T& b);

template<class T, class Compare>
const T& max(const T& a, const T& b, Compare comp);

So the basic idea here is that the function receives two objects by reference, and returns a reference to one of those objects. If it received objects of two different types, it wouldn't be able to return a reference to an input object because one of the objects would necessarily be of a different type than it was returning (so @bolov is correct about that part, but I don't think it's really the whole story).

With a modern compiler/standard library, if you don't might dealing with values instead of references, you could pretty easily write code on this general order:

template <class T, class U>
std::common_type<T, U> min(T const &a, U const &b) { 
    return b < a ? b : a;
}

template <class T, class U>
std::common_type<T, U> max(T const &a, U const &b) { 
    return a < b ? b : a;
}

That makes it pretty easy to deal with your case of passing an int and a long (or other pairs of types, as long as std::common_type can deduce some common type for them, and a<b is defined for objects of the two types.

But, in 1998, even if std::common_type had been available so it was easy to do, that solution probably wouldn't have been accepted (and as we'll see, it's still open to some question whether it's a great idea)--at the time, many people still thought in terms of lots of inheritance, so it was (more or less) taken for granted that you'd frequently use it in situations where both arguments were really of some derived type, something on this general order:

class Base { 
// ...
    virtual bool operator<(Base const &other);
};

class Derived1 : public Base {
    // ...
};
class Derived2 : public Base {
    // ...
};

Derived1 d1;
Derived2 d2;

Base &b = std::max(d1, d2);

In this case, the version above that returns a value instead of returning a reference would cause a serious problem. common_type<Derived1, Derived2> is going to be Base, so we'd end up slicing the argument to create an object of type Base, and returning that. This would rarely provide desirable behavior (and in some cases, such as if Base were an abstract base class, it wouldn't even compile).

There's one other point that's probably worth noting: even when applied in a seemingly simple situation, std::common_type can produce results you might not expect. For example, let's consider calling the template defined above like:

auto x = min(-1, 1u);

That leaves us with an obvious question: what type will x be?

Even though we've passed it an int and an unsigned, the type of the result is (at least potentially) neither int or unsigned (e.g., quite possibly long long)!

Related