ATemp
ATemp

Reputation: 319

C++: overloading does not choose expected method

I have the following code:

#include <iostream>
#include <vector>
using namespace std;

struct A{};
struct B: public A {};

template <typename T>
void foo(const T& obj) { cerr << "Generic case"<< endl;}

void foo(const A& a) {
    cerr << "Specific case" << endl;
}

int main() {
    vector<int> v;
    foo(v);
    B b;
    foo(b);
    A a;
    foo(a);
}

Output is

Why is it that foo(const A& a) is not being chosen for the B object ?

Curiously enough, if I removed the templated method and just have the following:

#include <iostream>
#include <vector>

struct A{};
struct B: public A {};

//template <typename T>
//void foo(const T& obj) { cerr << "Generic case"<< endl;}

void foo(const A& a) {
    cerr << "Specific case" << endl;
}

int main() {
    B b;
    foo(b);
    A a;
    foo(a);
}

The code compiles and the output is:

Specific case
Specific case

Why is the presence of the templated method making such a difference?

Edit: How can I force the compiler to choose the free method for classes derived from A in the presence of the templated method?

Upvotes: 0

Views: 243

Answers (3)

Matthieu M.
Matthieu M.

Reputation: 299820

Yes, it is a bit surprising but inheritance and template don't mix so well when it come to overload resolution.

The thing is, when evaluating which overload should be selected, the compiler chooses the one that necessitates the least conversions (built-in to built-in, derived-to-base, calls to non-explicit constructors or conversion operators, etc...). The ranking algorithm is actually pretty complex (not all conversions are treated the same...).

Once the overloads are ranked, if the two top-most are ranked the same and one is a template, then the template is discarded. However, if the template ranks higher than the non-template (less conversions, usually), then the template is selected.

In your case:

  • for std::vector<int> only one overload matches, so it is selected.
  • for A two overloads match, they rank equally, the template one is discarded.
  • for B two overloads match, the template rank higher (no derived-to-base conversion required), it is selected.

There are two work-arounds, the simplest is to "fix" the call site:

A const& ba = b;
foo(ba);

The other is to fix the template itself, however this is trickier...

You can hardcode that for classes derived from A this is not the overload you wish for:

template <typename T>
typename std::enable_if<not std::is_base_of<A, T>::value>::type
foo(T const& t) {
  std::cerr << "Generic case\n";
}

However this is not so flexible...

Another solution is to define a hook. First we need some metaprogramming utility:

// Utility
template <typename T, typename Result = void>
struct enable: std::enable_if< std::is_same<T, std::true_type>::value > {}; 

template <typename T, typename Result = void>
struct disable: std::enable_if< not std::is_same<T, std::true_type>::value > {}; 

And then we define our hook and function:

std::false_type has_specific_foo(...);

template <typename T>
auto foo(T const& t) -> typename disable<decltype(has_specific_foo(t))>::type {
  std::cerr << "Generic case\n";
}

And then for each base class we want a specific foo:

std::true_type has_specific_foo(A const&);

In action at ideone.

It is possible in C++03 too, but slightly more cumbersome. The idea is the same though, an ellipsis argument ... has the worst rank, so we can use overload selection on another function to drive the choice of the primary one.

Upvotes: 4

Praetorian
Praetorian

Reputation: 109119

@pmr's answer explains why the templated function is preferred in your example. To force the compiler to pick your overload instead, you can make use of SFINAE to drop the templated function from the overload set. Change the templated foo to

template <typename T>
typename std::enable_if<!std::is_base_of<A, T>::value>::type
  foo(const T& obj) { cerr << "Generic case"<< endl;}

Now, if T is A or a class derived from A the templated function's return type is invalid and it will be excluded from overload resolution. enable_if is present in the type_traits header.

Upvotes: 1

pmr
pmr

Reputation: 59811

No conversion is necessary for the call to foo(const B&) which the template instantiation yields thus it is the better match.

When a function call is seen by the compiler, every base function template has to be instantiated and is included in the overload set along with every normal function. After that overload resolution is performed. There is also SFINAE, which allows an instantiation of a function template to lead to an error (such a function would not be added to the overload set). Of course, things aren't really that simple, but it should give the general picture.

Regarding your edit: There is only one method to call. What else could there be as output?

Upvotes: 11

Related Questions