C++ 多态性和派生类类型 - "ugly programming"指针类型转换



首先,我不确定如何在一行中描述我正在做的事情……因此标题略显模糊。

对于这个问题,我能给出的最简短的描述是"我有一个函数,它应该能够接受许多可能的类类型中的任何一个作为参数,并且这些类都是从基类派生的"。

具体来说,我有2类,它们都实现了不同类型的方法,它们相似但不完全相同。

也许我举个例子会更好?你会看到我用指针类型转换做了一些有点奇怪的事情。我不认为这些是好的编程实践。他们至少有点奇怪,我想知道是否有一种替代的,更好的方法来做事。

好的,下面是我尝试的一个简化的例子:

class device
{
    // Nothing here - abstract base class
}
class inputDevice : device // inherit publicly, but it doesn't matter
{
    virtual input* getInput() { return m_input; } // input is a class
}
class outputDevice : device
{
    virtual output* getOutput() { return m_output; } // output is also a class
}
class inputoutputDevice : public inputDevice, public outputDevice
{
    // Inherits the get methods from input and output types
}
// elsewhere in program
void do_something(device* dev, int mode_flag)
{
    if(mode_flag == 1) // just an example
    {
        input* = ((inputDevice*)dev)->getInput(); // doing strange things with pointers
    }
    else if(mode_flag == 2)
    {
        output* = ((outputDevice*)dev)->getOutput(); // strange things with pointers
    }
    else if(mode_flag == 3)
    {
    }
}

你可以看到,这里的微妙之处在于,函数的一些行为取决于我们处理的参数是输入设备还是输出设备。

我想我可以多次重载函数,但是可能有许多不同类型的输入、输出或输入和输出设备…这将是一个相当复杂的方法。

将"get"方法放入基类中似乎也不是一个好主意,因为如果设备是OUTPUT设备,派生类不应该具有getInput()方法。同样,INPUT设备不应该有getOutput()方法。从概念上讲,这似乎是不对的。

我希望我解释得足够清楚,没有犯任何错误。

扩展一下我的评论,如果你看一下这个输入/输出库参考,你会看到一个类图,它在某种程度上提醒了你的类层次结构:有一个基类(实际上是两个),一个"输入"类和"输出"类,还有一个"输入/输出"类继承自"输入"one_answers"输出"类。

然而,你从来没有真正直接引用基类std::basic_iosstd::ios_base,相反,只有一次使用引用std::ostream的任何输出流,std::istream的任何输入流(和std::iostream的任何输入和输出流)。

例如,要重载输入操作符>>,函数需要一个对std::istream对象的引用:
std::istream& operator>>(std::istream& input_stream, some_type& dest);

即使对于更通用的函数,您也可以引用std::istream, std::ostreamstd::iostream对象。你永远不要使用基类std::basic_ios,仅仅因为你有问题。


为了更多地与你的问题和如何解决它联系起来,使用两个函数重载,一个用于输入设备,一个用于输出设备。它更有意义,因为首先你不会有检查类型和类型转换的问题,而且这两个函数的操作方式会完全不同,这取决于你是在做输入还是输出,而试图将它们混合到一个函数中只会使代码更加难以维护。

所以你应该用例如

void do_something(inputDevice& device);

void do_something(outputDevice& device);

在你的函数do_something()中有一个有趣的设计问题:你假设设备的类型对应于模式参数,但是你没有办法验证它。

选项1:使用动态强制转换

首先,当你期望你的device类是多态的,你应该预见到一个虚拟析构函数。这将确保设备也是多态的。

然后你可以使用动态强制转换使你的代码可靠(这里我假设mode 3是用于输入/输出,但它只是为了一般的想法):

void do_something(device* dev, int mode_flag)
{
    if(mode_flag == 1 || mode_flag==3) // just an example
    {
        inputDevice* id=dynamic_cast<inputDevice*>(dev); // NULL if it's not an input device 
        if (id) {  
           input* in = id->getInput(); // doing strange things with pointers
        }
        else cout << "Invalid input mode for device"<<endl; 
    }
    if(mode_flag == 2 || mode_flag==3) 
    {
        outputDevice* od=dynamic_cast<outputDevice*>(dev);  
        if (od) {  
           output* out = od->getOutput(); 
        }
        else cout << "Invalid output mode for device"<<endl; 
    }
    // ...
}
选项2:make do_something a method

我不知道它有多复杂,但是如果你打算用任何一种设备做某事,你可以把它变成一个方法。

class device {
public:
    virtual void do_something(int mode_flag) = 0; 
    virtual ~device() {}
};

你会明白的。当然,您也可以使用一个全局do_something()函数来执行一般步骤,并根据设备类型调用部分的成员函数。

其他言论

注意你的inputoutputDevice从device继承了两次。只要在device中有一些成员,这就可能导致歧义。因此,我建议您考虑对设备类进行虚拟继承。

class inputDevice : public virtual device
...;
class outputDevice : public virtual device
...;

另一种方法可能是在设备中有一个更复杂的输入/输出接口:

class device {
public:  
    virtual bool can_input() = 0;    // what can the device do ? 
    virtual bool can_output() = 0; 
    virtual input* getInput() = 0; 
    virtual void  setOutput(output*) = 0; 
    virtual ~device() {};
};
class inputDevice : public virtual device {
    bool can_input() { return true; }
    bool can_output() { return false; }
    input* getInput() { return m_input; } // input is a class
    void  setOutput(output*) { throw 1; } // should never be called ! 
}; 
...
void do_something(device* dev, int mode_flag)
{
    if(mode_flag == 1 && dev->can_input() ) // just an example
        ...
    ...
}    

由于问题领域相当广泛,因此不可能给出确切的答案,但由于它提到了设备,linux内核设备模型可能适合。

请参阅linux-kernel Tag wiki以获得更深入的了解。看看LDD3,因为它是一本免费的电子书,你可以看看内核是如何工作的。

linux内核的一般概念是每个设备都由文件表示。因此,你的驱动导出一个文件描述符,它有一个虚函数表(见fs.h)。

最简单的字符设备之一是命名管道(见它的虚函数表,文件中也有所有的函数定义)。

一个简单的c++转换可以像这样:

struct abstract_dev {
   virtual int read(input *) { return -1; /* fail */ }
   virtual int write(output *) { return -1; /* fail */ }
   virtual int ioctl(int cmd, void **args) { return -1; }
};
struct input_dev : public abstract_dev {
  input_dev() : state(0) {}
  int state;
  int read(input *) override { 
    if (state != 2) {
      return -1;
    }
    /* do smth */ 
    return 0;
  }
  int ioctl(int cmd, void **args) override {
    if (cmd == 2) { state = 2;  return 0;}
    return -1;
  }
};

对于模式,内核使用ioctl系统调用来设置模式(作为控制平面)并将状态保存在设备驱动程序中。随后的读写会考虑到模式。在命名管道示例中,您可以通过设置FIONREAD值来更改内部缓冲区的大小。

我希望这能帮助你找到解决问题的办法。

最新更新