类似于 C# 中的模板化回调参数的逆变

Contravariance for templated callback parameter like in C#

设置

考虑两种类型,其中一种继承自另一种:

#include <iostream>

class shape {  };
class circle : Shape {  };

还有两个函数分别接受该类型的对象:

void shape_func(const shape& s) { std::cout << "shape_func called!\n"; }
void circle_func(const circle& c) { std::cout << "circle_func called!\n"; }

函数模板

现在我想要一个函数模板,我可以传递给它:

  1. 这些类型之一的对象(或其他类似的)
  2. 指向与传递的对象兼容的那些函数(或其他类似函数)之一的指针。

以下是我声明此函数模板的尝试:

template<class T>
void call_twice(const T& obj, void(*func)(const T&))
{
    func(obj);
    func(obj);
}

(实际上它的主体会做一些更有用的事情,但为了演示目的,我只是让它用传递的对象调用传递的函数两次。)

观察到的行为

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
}

预期行为

我希望第三次调用也能正常工作。

毕竟,由于 shape_func 接受任何 shape,它也接受 circle — 所以用 circle 代替 T 应该是可能的让编译器在没有冲突的情况下解析函数模板。

事实上,这是对应的泛型函数在 C# 中的行为方式:

// C# code
static void call_twice<T>(T obj, Action<T> func) { ... }

它可以被称为call_twice(c, shape_func)没有问题,因为用C#语言来说,Action<T>的类型参数Tcontravariant.

问题

这在 C++ 中可行吗?

也就是说,函数模板 call_twice 必须如何实现才能接受此示例中的所有三个调用?

一种方法是通过 SFINAE,最好的例子是:

#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);
}

输出

shape_func called!
circle_func called!
shape_func called!

工作原理

这个实现使用了一个简单的(也许太多了)练习,利用 std::enable_if in conjunction with std::is_base_of 来提供合格的重载解决方案,可能有两种不同的类型(一种是对象,另一种是提供函数的参数列表)。具体来说,这个:

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&))

表示此函数模板需要两个模板参数。如果它们是同一类型或 Bp 不知何故是 Dp 的基类,则提供一个类型(在本例中为 void)。然后我们使用该类型作为函数的结果类型。因此,对于第一次调用,推导后的实例化结果如下所示:

void call_fn(Shape const& obj, void (*pfn)(const Shape&))

这正是我们想要的。第二次调用产生了类似的实例化:

void call_fn(Circle const& obj, void (*pfn)(const Circle&))

第三个实例化将产生这个:

void call_fn(Circle const& obj, void (*pfn)(const Shape&))

因为DpBp不同,但是Dp是一个导数


失败案例

要看到此失败(正如我们希望的那样),请使用不相关的类型修改代码。只需从 Circle:

的 base-class 继承列表中删除 Shape
#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.
}

第三次调用的结果将是不匹配的函数调用。