什么是dynamic_cast上下文中的“功能查询”,为什么这有用?

时间:2013-11-06 21:30:41

标签: c++ dynamic-cast

我正在阅读关于dynamic_cast的一些C ++材料,并且以下实践被认为是错误的:

class base{};
class derived1 d1 :public base{};
class derived2 d2 :public base
{
public:
void foo(){}
};
void baz(base *b)
{
    if (derived2 *d2= dynamic_cast<derived2 *> (b) )
    {
     d2-> foo();
    }
}

对此的补救措施是使用空的纯虚拟基类来使用“功能查询”,如下所示:

class capability_query
{
public:
    virtual void foo()= 0;
};

class base{};
class derived1 d1 :public base{};
class derived2 d2 :public base, public capability_query
{
public:
    virtual void foo(){}
};
void baz(base *b)
{
    if (capability_query *cq= dynamic_cast<capability_query *> (b) )
    {
      cq-> foo();
    }
}

我的第一个问题是为什么第一个代码块被认为是坏的? 只有在foo函数中可以从b成功降低d2时,才会执行我看到baz的方式。那么这里的问题是什么?!

我的第二个问题是为什么第二个代码块被认为是好的?以及如何解决这个问题,我首先不了解这个问题。

仅供参考,我的谷歌搜索capability query已返回http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Capability_Query 这似乎基本上是代码块1而不是代码块2。我仍然不明白为什么额外的空基类被认为是更好的做法?

修改 这里是最好的答案,我能想到of.Since内baz我向下转换为指针类型,而不是引用,万一丧气不成功,我会得到一个空指针,而不是标准:: bad_cast。因此,假设强制转换并且我确实获得了NULL指针,但是如果我不应该执行Null->foo并且如果我可能忘记测试NULL,那么代码块1可能是个问题。 代码块2修复此问题的方法是添加一个空类。即使

dynamic_cast<capability_query *> (b)

失败,我得到一个空指针,你无法执行 null->foo因为在capability_query类内部,此foo方法是纯虚拟的。这只是一个猜想,但可能是我在正确的道路上?? !!

2 个答案:

答案 0 :(得分:1)

学术答案是在面向对象设计中,你不应该依赖于实现,即具体的类。相反,您应该依赖于 interfaces 抽象基类等高级组件。您可以阅读有关此design principle on Wikipedia的更多信息。

这样做的原因是解耦设计,使代码更易于管理和维护。

让我们看一个例子。你有一个基类和派生类:

struct Duck {
    virtual ~Duck() {}
};

struct MallardDuck : public Duck {
    void quack() const {
        std::cout << "Quack!" << std::endl;
    }
};

假设你有另一个带有参数Duck的函数的类。

struct SoundMaker {
    void makeSound(const Duck* d) {
        if (const MallardDuck* md = dynamic_cast<const MallardDuck*>(d)) {
            md->quack();
        }
    }
};

您可以使用以下类:

MallardDuck md;
SoundMaker sm;
sm.makeSound(&md);

哪个输出Quack!。 现在让我们添加另一个派生类RubberDuck

struct RubberDuck : public Duck {
    void squeak() const {
        std::cout << "Squeak!" << std::endl;
    }
};

如果您希望SoundMaker使用课程RubberDuck,则必须在makeSound中进行更改:

void makeSound(const Duck* d) {
    if (const MallardDuck* md = dynamic_cast<const MallardDuck*>(d)) {
        md->quack();
    } else if (const RubberDuck* rd = dynamic_cast<const RubberDuck*>(d)) {
        rd->squeak();
    }
}

如果你需要添加另一种鸭子并发出声音怎么办?对于您添加的每种新型鸭子,您必须在新鸭子类的代码和SoundMaker中进行更改。这是因为您依赖具体实施。如果你可以在不改变SoundMaker的情况下添加新的鸭子,那会不会更好?请查看以下代码:

struct Duck {
    virtual ~Duck() {}
    virtual void makeSound() const = 0;
};

struct MallardDuck : public Duck {
    void makeSound() const override {
        quack();
    }

    void quack() const {
        std::cout << "Quack!" << std::endl;
    }
};

struct RubberDuck : public Duck {
    void makeSound() const override {
        squeak();
    }

    void squeak() const {
        std::cout << "Squeak!" << std::endl;
    }
};

struct SoundMaker {
    void makeSound(const Duck* d) {
        d->makeSound(); // No dynamic_cast, no dependencies on implementation.
    }
};

现在您可以像以前一样使用两种鸭子类型:

MallardDuck md;
RubberDuck rd;
SoundMaker sm;
sm.makeSound(&md);
sm.makeSound(&rd);

您可以根据需要添加任意数量的鸭子类型,而无需在SoundMaker中更改任何内容。这是一个解耦设计,更容易维护。 这就是为什么下行和依赖具体类是不好的做法的原因,而只是使用高级接口(在一般情况下)。

在第二个示例中,您使用单独的类来评估派生类的请求行为是否可用。当你将行为控制代码分开(并且封装)时,这可能会更好一些。它仍然会为您的实现创建依赖关系,每次实现更改时,您可能需要更改行为控制代码。

答案 1 :(得分:1)

d2->foo()上调用foo的第一个示例违反了开放 - 封闭原则,在这种情况下,这意味着您应该能够在d2中添加或删除功能而无需更改代码baz(或其他任何地方)。代码:

void baz(base *b)
{
    if (capability_query *cq= dynamic_cast<capability_query *> (b) )
    {
      cq-> foo();
    }
}

表明baz取决于类d2的定义。如果某天删除了函数d2::foo(),则还必须修改函数baz,否则您将成为编译器错误。

但是,在改进版本中,如果作者决定通过删除基类foo来删除d2的capability_query功能,(或者确实如果foo能力是添加到类d1)函数baz不需要修改,运行时行为将自动正确。