在C++中为抽象类模板创建接口

31
我有以下代码。我有一个抽象的模板类Foo和两个子类(Foo1和Foo2),它们派生自模板的实例化。我希望在我的程序中使用指针,可以指向类型为Foo1或Foo2的对象,因此我创建了一个接口IFoo。
我的问题是,我不确定如何在接口中包含functionB,因为它依赖于模板实例化。是否可能通过接口访问functionB,或者我正在尝试做不可能的事情?
非常感谢您的帮助。
class IFoo {
    public:
        virtual functionA()=0;

};

template<class T>
class Foo : public IFoo{
    public:
        functionA(){ do something; };
        functionB(T arg){ do something; };
};

class Foo1 : public Foo<int>{
...
};

class Foo2 : public Foo<double>{
...
};
5个回答

23

你实际上在尝试不可能的事情。

问题的核心很简单:virtualtemplate不太搭配。

  • template是关于编译时代码生成的。您可以将其视为一种类型感知的宏加上一些用于元编程的技巧。
  • virtual涉及运行时决策,这需要一些工作。

virtual通常使用虚表来实现(想象一下一个列出方法的表格)。方法的数量需要在编译时确定,并在基类中定义。

然而,根据您的要求,我们需要一个无限大小的虚表,其中包含尚未见过且仅在未来几年内定义的类型的方法...这是不可能的。

如果可能呢?

那么,这只会没有意义。当我使用int调用Foo2时会发生什么?它不适用于它!因此,它违反了Foo2实现从IFoo继承所有方法的原则。

因此,如果您说明了真正的问题,那么我们可以在设计层面帮助您,而不是在技术层面上:)


我有两个类Foo1和Foo2。它们都包含相似的数据结构:一个包含整数,另一个包含有理数(一对整数)。它们有许多方法是相同实现的(针对int / int对),这些方法与其他(int / int对)数据类型本身进行交互。但是它们有一些不同实现的方法。我最初使用继承来实现所有内容,但是数据结构在子类中定义,因此我无法编写在基类中对其进行操作的成员函数,这导致了大量的代码重复。 - bishboshbash
我曾多次遇到相同的问题,尤其是在接口中包含许多方法和属性(10+)的类中。我的解决方法是为每个实例创建接口,并使用闭包来减少方法数量,例如ErrorCode ScalarParameterSet(ParamType t, float ParamValue),以统一所有设置器。如果您需要,我可以提供一个示例。 - Jens Munk

9
最简单的方法是将您的界面制作成模板化。
template <class T>
class IFoo {
    public:
        virtual void functionA()=0;
        virtual void functionB(T arg){ do something; };
};

template<class T>
class Foo : public IFoo<T>{
    public:
        void functionA(){ do something; };
        void functionB(T arg){ do something; };
};

6
这意味着我不能创建IFoo类型的指针并使它们指向Foo1或Foo2的实例化。 - bishboshbash
3
没问题,那是一个根本性的问题。functionB所接受参数的类型取决于实例化的模板类型,并且必须在编译时知道。 - Igor Zevaka
1
@bishboshbash,你的需求本身就是不合理的。Foo1和Foo2有不同的基类(IFoo<int>和IFoo<double>是两种不同的类型)。 - h9uest

5

由于functionB的参数类型必须事先知道,因此您只有一种选择:将其作为一种可以容纳每个可能的参数的类型。这有时被称为“顶级类型”,boost库具有any类型,可以接近顶级类型的功能。以下是可能有效的代码:

#include <boost/any.hpp>
#include <iostream>
using namespace boost;

class IFoo {
    public:
    virtual void functionA()=0;
    virtual void functionB(any arg)=0; //<-can hold almost everything
};

template<class T>
class Foo : public IFoo{
    public:
        void functionA(){  };
        void real_functionB(T arg)
        {
         std::cout << arg << std::endl;
        };
        // call the real functionB with the actual value in arg
        // if there is no T in arg, an exception is thrown!

        virtual void functionB(any arg)
        {
            real_functionB(any_cast<T>(arg));
        }
};

int main()
{
    Foo<int> f_int;
    IFoo &if_int=f_int;

    if_int.functionB(10);

    Foo<double> f_double;
    IFoo &if_double=f_double;
if_int.functionB(10.0);

}

不幸的是,any_cast 不知道常规的转换。例如,any_cast<double>(any(123)) 会抛出异常,因为它甚至不尝试将整数 123 转换为双精度浮点数。它并不关心转换,因为无法复制所有转换。因此有一些限制,但如果必要的话,可以找到解决方法。


1
要使用此答案,调用者(main())必须知道 IFoo 实例的实际子类型(if_intif_double),以便可以传入正确的参数。如果出错,就会抛出运行时异常!那么,为什么不让调用者使用 dynamic_cast 呢? - Karmastan
1
如果我让调用者使用dynamic_cast,那么我需要他知道Foo<int>,Foo<double>。但是抽象接口的好处是,我不需要告诉任何人接口实际上是如何实现的。它可以是来自共享库的私有第三方类型或函数体中的本地类型。 此外,boost的any_cast只能对错误的参数类型抛出异常,这是它的限制。您可以自由增强其功能,以使其对内部类型进行正确的转换。但是,无法捕获所有有效的转换。 - Nordic Mainframe

0

您可以通过将IFoo*指针封装在一个类中,并通过非模板包装类的通用模板函数公开功能来实现类似的功能:

#include <assert.h>

// interface class
class IFoo {
public:
    virtual int type() const = 0; // return an identifier for the template parameter
    virtual bool functionA() = 0;
};

// This function returns a unique identifier for each supported T
template <typename T> static int TypeT() { static_assert("not specialized yet"); }
template <> static int TypeT<bool>() { return 0; }
template <> static int TypeT<double>() { return 1; }
//template <> static int TypeT<...>() { ... }

// templated class
template <typename T> class FooT : public IFoo {
public:
    int type() const override { return TypeT<T>(); }

    bool functionA() override { return true; }

    // not in interface
    bool functionB(T arg) { return arg == T(); }
};

// function to create an instance of FooT (could also be static function in FooT)
static IFoo* CreateFooT(int type)
{
    switch (type)
    {
    case 0: return new FooT<bool>();
    case 1: return new FooT<double>();
    //case ...: return new FooT<...>();
    default: return nullptr;
    }
}


// Non-templated wrapper class
class FooWrapper {
private:
    IFoo *pFoo;
public:
    FooWrapper(int type) : pFoo(CreateFooT(type)) { assert(pFoo != nullptr); }
    ~FooWrapper() { delete pFoo; }

    bool functionA() { return pFoo->functionA(); }

    template <typename T> bool functionB(T arg)
    {
        if(pFoo->type() != TypeT<T>())
        {
            assert(pFoo->type() == TypeT<T>());
            return false;
        }
        return static_cast<typename FooT<T>*>(pFoo)->functionB(arg);
    }



    // fun stuff:
    // (const pendants omitted for readability)

    bool changeType(int type)
    {
        delete pFoo;
        pFoo = CreateFooT(type);
        return pFoo != nullptr;
    }

    IFoo* Interface() { return pFoo; }

    IFoo* operator->() { return pFoo; }

    operator IFoo&() { return *pFoo; }

    template <typename T> FooT<T> *InterfaceT()
    {
        if(pFoo->type() != TypeT<T>())
        {
            assert(pFoo->type() == TypeT<T>());
            return nullptr;
        }
        return static_cast<typename FooT<T>*>(pFoo);
    }
};

int main(int argc, char *argv[])
{
    FooWrapper w1(TypeT<bool>());
    FooWrapper w2(TypeT<double>());

    w1.functionA(); // ok
    w2.functionA(); // ok

    w1.functionB(true); // ok
    w1.functionB(0.5); // runtime error!

    w2.functionB(true); // runtime error!
    w2.functionB(0.5); // ok


    // fun stuff
    w2.changeType(TypeT<bool>()); // older changes will be lost
    w2.functionB(true); // -> now ok

    w1.Interface()->functionA();
    w1->functionA();

    IFoo &iref = w1;
    iref.functionA();

    FooT<bool> *ref = w1.InterfaceT<bool>();
    ref->functionB(true);

    return 0;
}

当然,调用函数时使用正确的类型是您的责任,但您可以轻松地添加一些错误处理。


0

我认为你无法得到你想要的东西。如果你实现了你的建议,想象一下这种情况:如果你有一个指向 IFoo 实例的指针,并调用 functionB(),你应该给它什么类型的参数?根本问题在于 Foo1::functionB 和 Foo2::functionB 有不同的签名并执行不同的操作。


但从技术上讲,我不会创建IFoo的实例,我只想创建一个指向Foo1(或2)的实例(由IFoo指针指向),并且我假设适当的函数可以通过动态绑定找到? - bishboshbash
@bishboshbash:为了使函数能够被动态绑定找到,所有参数类型都必须是已知的;否则,如果您使用重载函数,将不清楚要查找什么。 - liori
我想我明白了。如果我创建了Foo1的实例并调用Foo1.functionB(..),那么它应该可以正常工作,因为模板已经被实例化了。我能否通过多重继承创建一个抽象类来指向类型为Foo1或Foo2的对象,从而避免动态绑定尝试通过未实例化的模板传递的问题? - bishboshbash

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接