代码之家  ›  专栏  ›  技术社区  ›  smls

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

  •  2
  • smls  · 技术社区  · 6 年前

    设置

    #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 -所以通过替换 圆圈 对于 T 编译器应该可以在不发生冲突的情况下解析函数模板。

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

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

    问题

    在C++中这是可能的吗?

    call_twice 必须实现才能接受本例中的所有三个调用?

    1 回复  |  直到 6 年前
        1
  •  1
  •   WhozCraig    6 年前

    一种方法是通过 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 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&))
    

    因为 是不同的,但是 是一种衍生物。


    失败案例

    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.
    }
    

    结果是第三次调用没有匹配的函数调用。