Consider two types, one of which inherits from the other:
#include <iostream>
class shape { };
class circle : Shape { };
And two functions which accept an object of that type, respectively:
void shape_func(const shape& s) { std::cout << "shape_func called!\n"; }
void circle_func(const circle& c) { std::cout << "circle_func called!\n"; }
Now I want a function template, to which I can pass:
The following is my attempt at declaring this function template:
template<class T>
void call_twice(const T& obj, void(*func)(const T&))
{
func(obj);
func(obj);
}
(In practice its body would do something more useful, but for demonstration purpose I simply let it call the passed function with the passed object twice.)
int main() {
shape s;
circle c;
call_twice<shape>(s, &shape_func); // works fine
call_twice<circle>(c, &circle_func); // works fine
//call_twice<circle>(c, &shape_func); // compiler error if uncommented
}
I was hoping that the third call would also work.
After all, since shape_func
accepts any shape
, it also accepts a circle
— so by substituting circle
for T
it should be possible for the compiler to resolve the function template without conflicts.
In fact, this is how the corresponding generic function behaves in C#:
// C# code
static void call_twice<T>(T obj, Action<T> func) { ... }
It can be called as call_twice(c, shape_func)
without problem because, to say it in C# lingo, the type parameter T
of Action<T>
is contravariant.
Is this possible in C++?
That is, how would the function template call_twice
have to be implemented in order to accept all three of the calls in this example?
One way to do this is via SFINAE, best shown by example:
#include <iostream>
#include <type_traits>
struct Shape {};
struct Circle : public Shape {};
template<class Bp, class Dp>
std::enable_if_t<std::is_base_of<Bp,Dp>::value,void>
call_fn(Dp const& obj, void (*pfn)(const Bp&))
{
pfn(obj);
}
void shape_func(const Shape& s) { std::cout << "shape_func called!\n"; }
void circle_func(const Circle& s) { std::cout << "circle_func called!\n"; }
int main()
{
Shape shape;
Circle circle;
call_fn(shape, shape_func);
call_fn(circle, circle_func);
call_fn(circle, shape_func);
}
Output
shape_func called!
circle_func called!
shape_func called!
How It Works
This implementation uses a simple (perhaps too much so) exercise utilizing std::enable_if
in conjunction with std::is_base_of
to provide qualified overload resolution with potentially two different types (one of the object, the other of the provide function's argument list). Specifically, this:
template<class Bp, class Dp>
std::enable_if_t<std::is_base_of<Bp,Dp>::value,void>
call_fn(Dp const& obj, void (*pfn)(const Bp&))
says that this function template requires two template arguments. If they're either the same type OR Bp
is somehow a base of Dp
, then provide a type (in this case void
). We then use that type as the result type of our function. Therefore, for the first invocation, the resulting instantiation looks like this after deduction:
void call_fn(Shape const& obj, void (*pfn)(const Shape&))
which was what we desired. A similar instantiation results from the second invocation:
void call_fn(Circle const& obj, void (*pfn)(const Circle&))
The third instantiation will produce this:
void call_fn(Circle const& obj, void (*pfn)(const Shape&))
because Dp
and Bp
are different, but Dp
is a derivative.
Failure Case
To see this fail (as we want it to do so), modify the code with non-related types. Simply remove Shape
from the base-class inheritance list of Circle
:
#include <iostream>
#include <type_traits>
struct Shape {};
struct Circle {};
template<class Bp, class Dp>
std::enable_if_t<std::is_base_of<Bp,Dp>::value,void>
call_fn(Dp const& obj, void (*pfn)(const Bp&))
{
pfn(obj);
}
void shape_func(const Shape& s) { std::cout << "shape_func called!\n"; }
void circle_func(const Circle& s) { std::cout << "circle_func called!\n"; }
int main()
{
Shape shape;
Circle circle;
call_fn(shape, shape_func); // still ok.
call_fn(circle, circle_func); // still ok.
call_fn(circle, shape_func); // not OK. no overload available,
// since a Circle is not a Shape.
}
The result would be no-matching function call for the third invoke.