通过抽象模板基类接口指针访问派生类方法,而无需在接口中显式类型

accessing derived class method via abstract template base class interface pointer, without explicit type in interface

本文关键字:接口 类型 派生 抽象 基类 访问 指针 类方法      更新时间:2023-10-16

这是我的第一篇文章。我花了几个小时检查我的问题的解决方案,在 SO 上搜索一个又一个链接,但没有一个准确地描述我的问题(我能得到的最接近的是这个和这个)。所以,让我们开始工作吧!

描述:我必须实现一个专用类的集合,每个类都能够存储其类型的链表。另外(棘手的部分),我必须实现一个集合管理器,以便向集合添加更多专用类不会影响其代码。

让我解释一下到目前为止我所拥有的。

class IList {
    public:
    virtual IList& operator+( IList&) = 0;
    virtual void print() = 0;
    virtual int g_Size() const = 0;
//perfect till here
    virtual void Push(const int&) = 0;//needs to be upgraded
    virtual const int& operator[](int index) = 0;//needs to be upgraded
};
template<class T>
class Queue: public IList{
    //internal stuff
public:
    Queue();
    int g_Size() const;
    void print();
    void Push(const T& cv);
    const T& operator[](int index);
    ~Queue();
};//all implementation of Queue<T> is implemented and working, but removed for simplicity
class CIntList : public Queue<int>{
    //stuff here, specialized on int
};
class C_Manager{
    IList * classes[3];//notice the polymorphism, managing the class collection using a pointer to the common(base) interface
public:
    void testing()
    {
        for (int i = 0; i < 3; i++)
            classes[i] = new CIntList(i);
        classes[0]->Push(1); classes[0]->Push(2); classes[1]->Push(1121); classes[2]->Push(12);
        classes[0]->print();
        classes[2]->print();
        int a = classes[0]->operator[](1);
        classes[1]->Push(a + a);
    } //working fine
};

好吧,所以你可能会问,问题是什么

不想为我的所有类专用化重新声明Pushoperator[](或任何其他使用模板作为参数的函数)。更确切地说,如果我想补充,让我们说,

class CFloatList: public Queue<float>
{
      //float stuff goes here
};

我还必须将IList修改为

class IList {
        public:
        virtual IList& operator+( IList&) = 0;
        virtual void print() = 0;
        virtual int g_Size() const = 0;
    //perfect till here
        virtual void Push(const int&) = 0;//used for int
        virtual const int& operator[](int index) = 0;//used for int
    //NEW DECLARATION FOR FLOAT
        virtual void Push(const float&) = 0;//used for float
        virtual const float& operator[](int index) = 0;//used for float
    };

如何避免这些重新声明?我需要某种"虚拟函数模板",但这在C++中不受支持。

我的方法错了吗?

很抱歉没有突出显示 c++ 语法,这是我的第一篇文章,我只设法将其格式化为代码块。谢谢你的时间!

编辑 #1更好的解决方案(正如JaggedSpire所建议的那样 - 非常感谢)

我已将IList修改为

class IList {
    public:
    virtual IList& operator+( IList&) = 0;
    virtual void afis() = 0;
    virtual int g_Size() const = 0;

    //templates
    template<typename T>
    void Push(const T& arg) //WORKS PERFECTLY
    {
        Queue<T>* cast = dynamic_cast<Queue<T>*>(this);
        cast->Push(arg);
    }
    template<typename T>
    const T& operator[](int index) //error
    {
        Queue<T>* cast = dynamic_cast<Queue<T>*>(this);
        return cast->operator[](index);
    }
};

void C_Manager::testing()

class C_Manager{
    public:
        void testing()
        {
            IList * a = new CIntList(1);
            a->Push(200);//WORKS PERFECTLY
            int c = a->operator[](0); //ERROR
        }
    };

它会产生这些错误

Error   C2783   'const T &IList::operator [](int)': could not deduce template argument for 'T'
Error   C2672   'IList::operator []': no matching overloaded function found
intellisense: no instance of function template "IList::operator[]" matches the argument list

基本上,它抱怨每个可能具有与 T 相关的返回类型的模板化函数。如何解决此问题以使我的经理真正多态?

首先,让我们回顾一下您的要求:

  • 具有非模板化多态基类,IList
  • 有一个类模板,Queue<T>从基类继承来实现它。
  • 能够随心所欲地专攻Queue<T>
  • 据推测,假设您从 push 返回 void,并从 operator[] 返回const T&,您希望发出异常信号。
  • 将特定类型的参数传递给基类IList,并让生成的行为取决于基础Queue<T>类型是否与给定参数的类型匹配。

最后一点是关键:您尝试根据调用者的运行时类型和参数的静态类型来选择函数的行为。但是,哪种类型实际与实现Queue<T>中的T匹配是在运行时确定的。

基于两个对象的运行时类型(因为参数在运行时和编译时是已知的)来确定行为的运行时是多方法的用途。C++没有本机多方法支持,但可以拼凑在一起dynamic_cast

通过这个答案,我发现了与您当前问题的相似之处,它提供了一系列精彩的链接,以获取有关在C++中实现(和实现)完整多方法功能的更多详细信息。

现在,在C++中对多方法进行暴力/朴素实现将需要从实现类型列表中测试每个可能的实现类型的参数。这也是你也表示你不想要的东西,但不用担心:你不需要。这是因为我们只想测试一种情况,而不是典型的多方法情况所需的多种情况。我们会在编译时获得要添加的参数类型,此时我们可以方便地使用该信息来查找我们感兴趣的唯一目标类型的类型。

对于提供的T类型,我们要测试要调度到的类型是否真的Queue<T>

为此,我们将使用与更简单的多方法实现相同的测试:dynamic_cast 。具体来说,我们将使用提供的参数类型作为所需模板参数的源,将this指针强制转换为我们正在测试的类型。

请注意:这意味着如果没有显式模板参数,类型之间的隐式转换就不会发生。如果将字符串文本传递给std::string容器,并且没有显式指定需要std::string容器,它将查找一个包含字符串文本长度的字符数组的容器,并且不检测到任何字符数组。毕竟,它们是不同的类型。

话虽如此,让我们进入代码。对于由各种Child<T>实现的接口Parent,您可以使用它从只能通过Parent接口访问的Child<T>中获取T特定行为:

class Parent{
    public:
    template <typename T>
    void foo(const T& t);
    virtual ~Parent(){}
};
template <typename T>
class Child : public Parent{
    public:
    void foo(const T& t);
};
// must be after the definition of the Child template, 
// because dynamic_cast requires a complete type to target
template <typename T>
void Parent::foo(const T& t){
    // throws on bad conversion like we want
    auto castThis = dynamic_cast<Child<T>&>(*this); 
    // if execution reaches this point, this is a Child<T>
    castThis.foo(t);
}

跟:

template<typename T>
void Child<T>::foo(const T& t){
    std::cout << typeid(T).name() << ": " << t << 'n';
}

int main(){
    Parent&& handle = Child<int>();
    try{
        handle.foo<int>(3);
        handle.foo<char>(0);
        handle.foo<std::string>("Hello!");
    }
    catch(std::bad_cast e){
        std::cout << "bad cast caughtn";
    }
}

我们在 g++ 5.2.0 和 clang 3.7 上得到以下输出

i: 3
bad cast caught

这就是我们想要的。

获得此处介绍的简单多态接口后,实现集合应该很容易。我会自己围绕一个std::vector<std::unique_ptr<Parent>>进行包装类,但这个决定最终取决于你。


现在,因为这还不够一堵文字墙,一些注释:

  1. 引发异常不利于标准控制流。如果您实际上不知道参数是否通过某些外部逻辑与基础类型匹配,则需要某种其他形式的错误处理。 dynamic_cast可用于强制转换引用和指针。强制转换对不属于目标类型的对象的引用将引发std::bad_cast 。强制转换指针将返回空指针
  2. 对派生类中的成员函数使用与基类中调用该成员函数的模板化成员函数相同的名称,因为名称查找在C++中的工作方式。从这个答案:

    基本算法是编译器将从当前值的类型开始,然后沿着层次结构向上,直到它在具有目标名称的类型上找到成员。然后,它将仅对具有给定名称的该类型的成员执行重载解析。它不考虑父类型上同名的成员。

所以foo的查找将从Child<T>开始,并且由于它在Child<T>中找到具有该名称的成员函数,因此它不会再次检查Parent或调用调度函数。
3.在实际使用这种解决方法之前,我会考虑为什么要仔细执行此操作。