1

我正在阅读一些关于 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只有在 d2 可以从baz函数中的 b 成功向下转换时才会执行。那么这里的问题是什么?!

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

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

编辑: 这是我能想到的最好的答案。因为在里面baz我向下转换为指针类型而不是引用,如果向下转换不成功,我将得到一个空指针而不是 std::bad_cast。所以,假设转换出错并且我确实得到了 NULL 指针,但是如果我不应该执行Null->foo并且如果我可能忘记测试 NULL 怎么办,那么代码块 1 可能是一个问题。代码块 2 解决此问题的方法是添加一个空类。即使

dynamic_cast<capability_query *> (b)

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

4

2 回答 2

1

第一个示例,其中调用 foo d2->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能力被添加到 class d1)该函数baz不需要修改,并且运行时行为将自动正确的。

于 2014-04-15T01:12:35.183 回答
1

学术上的答案是,在面向对象的设计中,你不应该依赖于实现,即具体的类。相反,您应该依赖高级组件,例如接口抽象基类。您可以在 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. 这是一种解耦设计,更容易维护。这就是为什么向下转换和依赖具体类是不好的做法的原因,而不是只使用高级接口(在一般情况下)。

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

于 2013-11-06T23:07:30.250 回答