【问题标题】:Contravariance for templated callback parameter like in C#C# 中模板化回调参数的逆变性
【发布时间】:2018-08-23 20:07:31
【问题描述】:

设置

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

#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&lt;T&gt;的类型参数Tcontravariant

问题

这在 C++ 中可行吗?

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

【问题讨论】:

  • 不确定我是否关注,你的意思是something like this?
  • @WhozCraig:是的,这就是我想要的行为!谢谢!你能把它作为答案发布并解释一下吗?

标签: c++ contravariance


【解决方案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是派生词。


失败案例

要看到这个失败(正如我们希望的那样),用不相关的类型修改代码。只需从Circle 的基类继承列表中删除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.
}

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-07
    • 2016-12-25
    • 1970-01-01
    相关资源
    最近更新 更多