dynamic_cast的用例

use*_*414 25 c++ pointers reference

在许多地方你可以读到这dynamic_cast意味着"糟糕的设计".但我找不到任何具有适当用法的文章(显示出良好的设计,而不仅仅是"如何使用").

我正在编写一个带有电路板的棋盘游戏和许多不同类型的卡,这些卡具有许多属性(一些卡可以放在板上).所以我决定将其分解为以下类/接口:

class Card {};
class BoardCard : public Card {};
class ActionCard : public Card {};
// Other types of cards - but two are enough
class Deck {
    Card* draw_card();
};
class Player {
    void add_card(Card* card);
    Card const* get_card();
};
class Board {
    void put_card(BoardCard const*);
};
Run Code Online (Sandbox Code Playgroud)

有些人建议我只使用一个描述卡片的课程.但我的意思是许多相互排斥的属性.对于Board类' put_card(BoardCard const&)- 它是接口的一部分,我不能把任何卡放在板上.如果我只有一种类型的卡,我将不得不在方法内检查它.

我看到的流程如下:

  • 一个通用的卡是在甲板上(这并不重要它的类型是什么)
  • 从牌组中抽取一张通用牌并交给一名牌手(与上述相同)
  • 如果玩家选择了BoardCard,那么它可以被放在棋盘上

所以我dynamic_cast在把卡片放在板子上之前使用.我认为在这种情况下使用一些虚拟方法是不可能的(另外我没有任何意义为每张卡添加一些关于板的操作).

所以我的问题是:我的设计是什么?我怎么能避免dynamic_cast?使用某些类型属性和ifs将是一个更好的解决方案......?

PS dynamic_cast在设计环境中处理使用的任何来源都是值得赞赏的.

mik*_*osz 13

是的,dynamic_cast是一种代码味道,但是添加的功能也是如此,这些功能试图让它看起来像你有一个很好的多态接口,但实际上是等同于dynamic_cast类似的东西can_put_on_board.我甚至会说这can_put_on_board更糟糕 - 你正在复制代码,否则dynamic_cast会使界面变得混乱.

与所有代码气味一样,它们应该让你警惕,并不一定意味着你的代码是坏的.这一切都取决于你想要达到的目标.

如果你正在实现一个有5k行代码,两类卡片的棋盘游戏,那么任何可行的东西都可以.如果您正在设计更大,可扩展且可能允许由非程序员创建卡片的东西(无论是实际需要还是您正在进行研究),那么这可能不会.

假设后者,让我们看一些替代方案.

您可以将卡的适当责任放在卡上,而不是一些外部代码.例如play(Context& c),向卡添加功能(Context作为访问板的手段和可能需要的任何东西).板卡会知道它可能只适用于板子而且不需要演员.

但我完全放弃使用继承.它的许多问题之一是它如何引入所有卡的分类.让我给你举个例子:

  • 您介绍BoardCardActionCard把所有的卡在这两个桶;
  • 然后,你决定要一张可以两种方式使用的卡片,无论ActionBoard卡片还是卡片;
  • 让我们说你解决了这个问题(通过多重继承,一种BoardActionCard类型或任何不同的方式);
  • 然后你决定要有卡片颜色(如在MtG中) - 你是怎么做到的?你创建RedBoardCard,BlueBoardCard,RedActionCard等?

应该避免继承的原因以及如何实现运行时多态性的其他示例,否则您可能希望观看Sean Parent的优秀"继承是邪恶的基类"谈话.实现这种多态性的有前途的库是dyno,但我还没有尝试过.

可能的解决方案可能是:

class Card final {
public:
    template <class T>
    Card(T model) :
        model_(std::make_shared<Model<T>>(std::move(model)))
    {}

    void play(Context& c) const {
        model_->play(c);
    }

    // ... any other functions that can be performed on a card

private:

    class Context {
    public:
        virtual ~Context() = default;
        virtual void play(Context& c) const = 0;
    };

    template <class T>
    class Model : public Context {
    public:
        void play(Context& c) const override {
            play(model_, c);

            // or

            model_.play(c);

            // depending on what contract you want to have with implementers
        }
    private:
        T model_;
    };

    std::shared_ptr<const Context> model_;

};
Run Code Online (Sandbox Code Playgroud)

然后,您可以为每种卡类型创建类:

class Goblin final {
    void play(Context& c) const {
        // apply effects of card, e.g. take c.board() and put the card there
    }
};
Run Code Online (Sandbox Code Playgroud)

或者实现不同类别的行为,例如有一个

template <class T>
void play(const T& card, Context& c);
Run Code Online (Sandbox Code Playgroud)

模板然后使用enable_if来处理不同的类别:

template <class T, class = std::enable_if<IsBoardCard_v<T>>
void play(const T& card, Context& c) {
    c.board().add(Card(card));
}
Run Code Online (Sandbox Code Playgroud)

哪里:

template <class T>
struct IsBoardCard {
    static constexpr auto value = T::IS_BOARD_CARD;
};

template <class T>
using IsBoardCard_v = IsBoardCard<T>::value;
Run Code Online (Sandbox Code Playgroud)

然后定义你Goblin的:

class Goblin final {
public:
    static constexpr auto IS_BOARD_CARD = true;
    static constexpr auto COLOR = Color::RED;
    static constexpr auto SUPERMAGIC = true;
};
Run Code Online (Sandbox Code Playgroud)

这将允许您在许多方面对您的卡进行分类,也可以通过实现不同的play功能完全专门化行为.

示例代码使用std :: shared_ptr来存储模型,但您绝对可以在这里做更聪明的事情.我喜欢使用静态大小的存储空间,只允许使用特定最大尺寸和对齐的Ts.或者,您可以使用std :: unique_ptr(虽然会禁用复制)或利用小尺寸优化的变体.

  • 我认为你有一个小错误,可能会让一些人感到困惑(它让我感到困惑).不应该`template <class T> class Model`继承自`Context`? (2认同)

小智 0

我哪里设计的不好?

Card问题是,每当引入新类型时,您总是需要扩展该代码。

我怎样才能避免dynamic_cast?

避免这种情况的通常方法是使用接口(即纯抽象类):

struct ICard {
   virtual bool can_put_on_board() = 0;
   virtual ~ICard() {}
};

class BoardCard : public ICard {
public:
    bool can_put_on_board() { return true; };
};

class ActionCard : public ICard {
public:
    bool can_put_on_board() { return false; };
};
Run Code Online (Sandbox Code Playgroud)

这样,您可以简单地使用引用或指针来ICard检查它所保存的实际类型是否可以放在Board.


但我找不到任何具有适当用法的文章(展示良好的设计,而不仅仅是“如何使用”)。

总的来说,我想说动态转换没有任何好的、现实生活中的用例。

有时我在 CRTP 实现的调试代码中使用它,例如

template<typename Derived> 
class Base {
public:
    void foo() {
#ifndef _DEBUG     
      static_cast<Derived&>(*this).doBar();
#else
      // may throw in debug mode if something is wrong with Derived
      // not properly implementing the CRTP
      dynamic_cast<Derived&>(*this).doBar();
#endif
    }
};
Run Code Online (Sandbox Code Playgroud)