【问题标题】:overloading function and inheritance重载函数和继承
【发布时间】:2023-04-04 05:20:01
【问题描述】:

如果我使用给定的类 MyClass 或任何派生类 MyClassDer 调用它,我正在尝试重载某些模板函数以执行特定操作。代码如下:

#include <iostream>

struct MyClass {
    virtual void debug () const {
        std::cerr << "MyClass" << std::endl;
    };
};

struct MyClassDer : public MyClass {
    virtual void debug () const {
        std::cerr << "MyClassDer" << std::endl;
    };
};

template <typename T> void  func  (const T& t) {
    std::cerr << "func template" << std::endl;
}

void func (const MyClass& myClass) {
    std::cerr << "func overloaded" << std::endl;
    myClass.debug ();
}


int main(int argc, char **argv) {
    func (1);
    MyClass myClass;
    func (myClass);
    MyClassDer myClassDer;
    func (myClassDer);
}

输出是:

func template
func overloaded
MyClass
func template

func (myClassDer) 调用模板函数而不是 void func (const MyClass&amp; myClass)。我该怎么做才能获得预期的行为?

谢谢

【问题讨论】:

    标签: c++ templates inheritance sfinae overload-resolution


    【解决方案1】:

    这就是重载解析的工作原理。当查找完成时,它会同时找到模板和函数。然后推导出模板类型并开始重载决议。对于MyClass 类型的参数,两个候选者是:

    void func<MyClass>(MyClass const&);
    void func(MyClass const&);
    

    这对于参数来说同样是很好的匹配,但第二个是非模板是首选。在MyClassDer的情况下:

    void func<MyClassDer>(MyClassDer const&);
    void func(MyClass const&);
    

    在这种情况下,第一个是比第二个更好的候选者,因为第二个需要派生到基础的转换,并且被选中。

    有不同的方法可以直接调度以命中您的代码。最简单的就是将参数的类型强制为MyClass,从而回退到原来的情况:

    func(static_cast<MyClass&>(myClassDer));
    

    虽然很简单,但需要在任何地方完成,如果你只忘记一个地方,就会调用错误的东西。其余的解决方案是复杂的,您可能要考虑是否只提供不同的函数名称会更好。

    当类型派生自 MyClass 时,其中一个选项是使用 SFINAE 禁用模板:

    template <typename T>
    typename std::enable_if<!std::is_base_of<MyClass,MyClassDer>::value>::type
    func(T const & t) { ... }
    

    在这种情况下,查找后,编译器会进行类型推导,将T推导出为MyClassDer,然后评估函数的返回类型(SFINAE也可以应用于另一个模板或函数参数)。 is_base_of 将产生 false 并且 enable_if 不会有嵌套类型。函数声明格式不正确,编译器将删除它,将解析集保留为单个候选者,即非模板重载。

    另一种选择是提供单个模板接口,并使用标签调度在内部调度模板或重载(使用不同的名称)。这个想法是相似的,你评估模板中的特征并调用一个函数,该函数具有从该评估生成的类型。

    template <typename T>
    void func_impl(T const&, std::false_type) {...}
    void func_impl(MyClass const&, std::true_type) {...}
    
    template <typename T>
    void func(T const &x) { 
       func_impl(x,std::is_base_of<MyClass,MyClassDer>::type()); 
    }
    

    还有其他选择,但这是两种常见的,其余的主要基于相同的原则。

    再次考虑问题是否值得解决方案的复杂性。除非对 func 的调用本身是在通用代码中完成的,否则只需简单更改函数名称即可解决问题,而不会不必要地增加您或其他维护者可能难以维护的复杂性。

    【讨论】:

    • 您是否在几秒钟内就输入了这个巨大的答案? +1
    • @MM.: 差不多几分钟……我不是打字速度慢,但我也不是 Flash
    • @MM。名称查找、参数推导和重载解析的三位一体经常出现,以至于在编写模板代码时,或多或少地在工作内存中使用它是值得的。参见例如Stephan T. Lavavej 的讲座Core C++
    • @TemplateRex:也就是说,如果您的大脑 L1 缓存有足够的容量用于查找规则……我的不确定。我在 L1 中维护简单查找、类型推断和简单分辨率,在 RAM 中维护其余分辨率和第二层查找,在 HD 中维护其余查找(字面意思是:标准 PDF)
    • 大声笑,这是你大脑记忆层次的一个很好的总结!无论如何,我敢打赌这个特定的答案直接来自 L1?
    【解决方案2】:

    关于为什么您的代码不起作用:请参阅@David 的出色解释。要使其工作,您可以通过添加隐藏模板参数Requires 来使用 SFINAE(“Substition Failure is not an Errro)(名称仅用于文档目的)

    template <
         typename T, typename Requires = typename 
         std::enable_if<!std::is_base_of<MyClass, T>::value, void>::type 
    > 
    void  func  (const T& t) {
        std::cerr << "func template" << std::endl;
    }
    

    当T 等于或派生自MyClass 时,这将禁用此模板进行重载解析,并改为选择常规函数(与模板参数相反,将执行从派生到基的转换扣除,仅考虑完全匹配)。您显然可以玩弄这个并在std::enable_if 内添加几个具有非重叠条件的重载,以便对将考虑的函数重载进行细粒度选择。但要小心,SFINAE 很微妙!

    Live Example.

    注意:我使用 C++11 语法编写了我的 SFINAE,使用函数模板的默认模板参数。在 C++98 中,您需要添加常规默认参数或修改返回类型。

    【讨论】:

      【解决方案3】:

      您可以使用 SFINAE:

      #include <type_traits>
      
      template <typename T>
      void func (const T& t, typename std::enable_if<!std::is_base_of<MyClass, T>::value>::type * = nullptr) {
          std::cout << "func template" << std::endl;
      }
      
      template <
          typename T
          , typename = typename std::enable_if<std::is_base_of<MyClass, T>::value>::type
      >
      void func (const T& t) {
          std::cout << "func overloaded" << std::endl;
          t.debug ();
      }
      

      如果您没有 C++11,boost 提供相同的功能。

      Live example

      编辑

      这应该可以在没有 C++11 的情况下工作(使用 boost):

      #include "boost/type_traits.hpp"
      
      template <typename T>
      void func (const T& t, typename boost::enable_if<!boost::is_base_of<MyClass, T>::value>::type * = 0) {
          std::cout << "func template" << std::endl;
      }
      
      template <typename T>
      void func (const T& t, typename boost::enable_if<boost::is_base_of<MyClass, T>::value>::type * = 0) {
          std::cout << "func overloaded" << std::endl;
          t.debug ();
      }
      

      【讨论】:

      • 这似乎是正确的解决方案。我将看看如何用 boost 来实现它。
      • @user2811040 boost::enable_if 和 boost::is_base_of。就是这样。
      • @user2811040 如果不使用 C++11,请将第二个模板参数也移动到指针技巧中。
      • 我现在还没有 C++11,也不清楚“将第二个模板参数也移动到指针技巧”。我想我需要深入研究一下 boost 文档。谢谢
      • @user2811040 我添加了一个 boost & C++03 版本
      【解决方案4】:

      多态发生在运行时,但选择重载函数发生在编译时。

      因此,在编译时接受MyClassDer 的最佳重载是

      func<MyClassDer> (const MyClassDer& t)
      

      而不是

      func<MyClass> (const MyClass& t)
      

      然后编译器选择第一个。


      解决问题的一种可能性是:

      func(static_cast<MyClass&>(myClassDer));
      

      【讨论】:

        【解决方案5】:

        您需要使用多态性才能调用您的模板函数。您需要对基类的引用:

        int main(int argc, char **argv) {
            func (1);
            MyClass myClass;
            func (myClass);
            MyClassDer myClassDer;
            MyClass* mc = &myClassDer;
            func (*mc);
        }
        

        More polymorphism examples and details here

        【讨论】:

        • 您不妨创建一个引用并删除几个*
        【解决方案6】:

        因为你的重载函数的签名是,

        void func (const MyClass& myClass)
        {
            std::cerr << "func overloaded" << std::endl;
            myClass.debug ();
        }
        

        即它想要MyClass 作为它的参数并且你使用MyClassDer 调用它。所以在编译时它会解析其他重载函数并与之链接。由于其他函数是模板化的,因此编译器与该函数链接没有问题。

        所以如果你想传递一个MyClassDer 对象,你仍然可以使用多态性来实现。

        MyClass *myClassDer = new MyClassDer;
        func(*myClassDer);
        

        【讨论】:

        • 不需要new 任何东西,你可以创建指向局部变量的指针(或者更好的是,一个引用)
        【解决方案7】:

        只需将其转换为基本类型:

        MyClassDer myClassDer;
        func(static_cast<MyClass&>(myClassDer));
        

        【讨论】:

        • static_cast 对我来说不是一个好的解决方案,因为我真的不能指望我的函数的用户使用这个构造。此外,我的函数实际上将是一个运算符!
        • @user2811040 啊。而且我想您希望您的函数的用户也能够自己扩展 MyClass 。在这种情况下,您似乎已经接受了正确的答案。
        【解决方案8】:
        MyClass *myClassDer = new MyClassDer;
        func(*myClassDer);
        delete myClassDer;
        

        【讨论】:

        • 有什么理由引入动态分配?
        • 函数重载在编译时解决,因此您需要MyClass 才能访问正确的函数。但是要使运行时多态性起作用,您实际上需要一个MyClassDer 的对象。因此new.
        • 使用动态分配不会改变任何东西。多态性适用于引用...
        • func(static_cast&lt;MyClass&amp;&gt;(myClassDer)); 有什么问题?除非需要,否则切勿使用new。
        • @Angew 谁说func(static_cast&lt;MyClass&amp;&gt;(myClassDer));有什么问题?这只是另一种方式。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-12-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-05-24
        相关资源
        最近更新 更多