模板回调参数的协方差,例如C#

时间:2018-08-23 20:07:31

标签: c++ contravariance

设置

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

#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#术语来说,T的类型参数Action<T>contravariant

问题

在C ++中有可能吗?

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

1 个答案:

答案 0 :(得分:1)

一种执行此操作的方法是通过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_ifstd::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&))

说此函数模板需要两个模板参数。如果它们是同一类型,或者BpDp的基数,则提供一个类型(在这种情况下为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是派生的。


故障案例

要查看失败(如我们所愿),请使用不相关的类型修改代码。只需从Shape的基类继承列表中删除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.
}

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