类似于 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"; }
函数模板
现在我想要一个函数模板,我可以传递给它:
- 这些类型之一的对象(或其他类似的)。
- 指向与传递的对象兼容的那些函数(或其他类似函数)之一的指针。
以下是我声明此函数模板的尝试:
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>
的类型参数T
是contravariant.
问题
这在 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&))
因为Dp
和Bp
不同,但是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.
}
第三次调用的结果将是不匹配的函数调用。
设置
考虑两种类型,其中一种继承自另一种:
#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"; }
函数模板
现在我想要一个函数模板,我可以传递给它:
- 这些类型之一的对象(或其他类似的)。
- 指向与传递的对象兼容的那些函数(或其他类似函数)之一的指针。
以下是我声明此函数模板的尝试:
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>
的类型参数T
是contravariant.
问题
这在 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&))
因为Dp
和Bp
不同,但是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.
}
第三次调用的结果将是不匹配的函数调用。