在运行时了解对象类型的首选方法

Preferred way to understand object type at runtime

考虑我有一个 Plant class 派生了 FruitVegetable classes,以及 Fruit class 有一些更派生的 classes,比如 OrangeApple,而 Vegetable 派生了 PotatoTomato。假设,PlantPlant::onConsume()=0; 方法:

class Plant
{
public:
    virtual void onConsume(void)=0;
};

class Fruit:public Plant
{
};

class Orange:public Fruit
{
    void onConsume(void)
    {
        // Do something specific here
    }
};

class Apple:public Fruit
{
    void onConsume(void)
    {
        // Do something specific here
    }
};

class Vegetable:public Plant
{
};

class Potato:public Vegetable
{
    void onConsume(void)
    {
        // Do something specific here
    }
};
class Tomato:public Vegetable
{
    void onConsume(void)
    {
        // Do something specific here
    }
};

class Consumer
{
public:
    void consume(Plant &p)
    {
        p.onConsume();
        // Specific actions depending on actual p type here
        // like send REST command to the remote host for Orange
        // or draw a red square on the screen for Tomato
    }
};

假设,我有一个 Consumer class 和 Consumer::consume(Plant) 方法。这种“消费”方法应该对不同的“植物”执行不同的操作instances/types,其中为任何“植物”调用Plant::onConsume()。这些操作与 Plant class 没有直接关系,需要很多不同的附加操作和参数,可以完全随意,因此不能在 onConsume 方法中实现。

实现它的首选方法是什么?据我了解,可以实施一些“Plant::getPlantType()=0”方法,这将 return 植物类型,但在这种情况下我不确定它应该是什么 return。如果 returned 值是一个枚举,我需要在每次添加新的派生 class 时更改此枚举。在任何情况下,都无法控制多个派生的 classes 可以 return 相同的值。

此外,我知道有一个 dynamic_cast 转换 returns nullptr 如果无法进行转换,以及 typeid() 运算符 returns std::typeinfo(即使使用 typeinfo::name()),也可以在 switch() 中使用(这对我的情况来说非常好)。但恐怕它会显着减慢执行速度并使代码更重。

那么,我的问题是,在 C++ 中执行此操作的首选方法是什么?也许我只是忘记了一些更简单的实现方法?

一点更新。感谢您对继承、封装等的解释!我以为我的问题很清楚,但事实并非如此,对此我感到抱歉。所以,请考虑一下,就像我无法访问整个 Plant 源层次结构一样,只需要实现这个 Consumer::onConsume(Plant)。所以我不能在其中添加新的特定方法。或者,它也可以被视为一个 Plants 库,我必须编写一次,并使其可供其他开发人员使用。因此,我可以将 cases/functionality 的使用分为两部分:一部分在 Plant::onConsume() 方法中实现了“per class”,第二部分是未知的,并且会根据使用情况而有所不同。

多态性就是不必知道特定类型。如果您发现必须明确检测特定类型,通常您的设计存在缺陷。

一开始:

void Consumer::consume(Plant p)

没有按预期工作! Plant 对象按值接受,即。 e.它的字节被一个一个地复制;但是, Plant 类型的那些,任何其他(派生类型的)都将被忽略并迷失在 consume 函数中 – 这称为 object slicing.

多态性仅适用于引用或指针。

现在假设您想执行如下操作(代码不完整!):

void Consumer::consume(Plant& p) // must be reference or pointer!
{
    p.onConsume();
    generalCode1();
    if(/* p is apple */)
    {
        appleSpecific();
    }
    else if(/* p is orange */)
    {
        orangeSpecific();
    }
    generalCode2();
}

你不想自己决定类型,你让 Plant class 为你做这些事情,这意味着你适当地扩展它的接口:

class Plant
{
public:
    virtual void onConsume() = 0;
    virtual void specific() = 0;
};

消费函数的代码现在将更改为:

void Consumer::consume(Plant const& p) // must be reference or pointer!
{
    p.onConsume();
    generalCode1();
    p.specific();
    generalCode2();
}

您可以在需要特定行为的任何地方执行此操作(specific 只是一个演示名称,请选择一个能够很好地描述该功能实际用途的名称)。

    p.onConsume();
    generalCode1();
    p.specific1();
    generalCode2();
    p.specific2();
    generalCode3();
    p.specific3();
    generalCode4();
    // ...

当然,您现在需要在派生的 classes 中提供适当的实现:

class Orange:public Fruit
{
    void onConsume() override
    { }
    void specific() override
    {
        orangeSpecific();
    }
};

class Apple:public Fruit
{
    void onConsume() override
    { }

    void specific() override
    {
        appleSpecific();
    }
};

注意添加了 override 关键字,它可以防止您意外创建重载函数,而不是在签名不匹配的情况下实际覆盖。如果您发现必须更改基中的函数签名,它也可以帮助您找到所有需要更改的地方 class。

一个选项是访问者模式,但这需要在某些 class 中每种类型一个函数。基本上,您创建一个基础 class PlantVisitor,每个对象类型有一个 Visit 函数,然后向 Plant 添加一个虚拟方法,该方法接收 PlantVisitor 对象并调用访问者将自己作为参数传递的相应函数:

class PlantVisitor
{
public:
    virtual void Visit(Orange& orange) = 0;
    virtual void Visit(Tomato& tomato) = 0;
    ...
};

class Plant
{
public:
    virtual void Accept(PlantVisitor& visitor) = 0;
};

class Orange : public Plant
{
public:
    void Accept(PlantVisitor& visitor) override
    {
        visitor.Visit(*this);
    }
};


class Tomato : public Plant
{
public:
    void Accept(PlantVisitor& visitor) override
    {
        visitor.Visit(*this);
    }
};

这将允许你做这样的事情:

class TypePrintVisitor : public PlantVisitor
{
public:
    void Visit(Orange& orange) override
    {
        std::cout << "Orange\n";
    }
    void Visit(Tomato& tomato) override
    {
        std::cout << "Tomato\n";
    }
};

std::vector<std::unique_ptr<Plant>> plants;
plants.emplace_back(std::make_unique<Orange>());
plants.emplace_back(std::make_unique<Tomato>());

TypePrintVisitor visitor;

for (size_t i = 0; i != plants.size(); ++i)
{
    std::cout << "plant " << (i+1) << " is a ";
    plants[i]->Accept(visitor);
}

虽然不确定是否需要这样做并不表示设计效率低下。

顺便说一句:如果您有多个访问者并且不一定要为所有访问者中的每个类型实现逻辑,您可以在 PlantVisitor 中添加默认实现来调用超类型的函数指定纯虚函数。