在许多地方,您可以读到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*);
};
有些人建议我只使用一个描述卡片的课程。但我的意思是许多相互排斥的属性。在Board类'put_card(BoardCard const&)
的情况下 - 它是界面的一部分,我不能把任何卡放在板上。如果我只有一种类型的卡,我将不得不在方法内检查它。
我看到的流程如下:
所以我在将卡片放在棋盘上之前使用dynamic_cast
。我认为在这种情况下使用某种虚拟方法是不可能的(另外我没有任何理由在每张卡上添加一些关于板的操作)。
所以我的问题是:我的设计是什么?我怎么能避免dynamic_cast
?使用某种类型属性和if
s将是更好的解决方案......?
P.S。
任何在设计环境中处理dynamic_cast
使用情况的来源都不胜感激。
答案 0 :(得分:13)
是的,dynamic_cast
是一种代码气味,但添加的功能也是如此,这些功能试图让它看起来像你有一个好的多态接口,但实际上等于dynamic_cast
,比如{{1} }}。我甚至会说can_put_on_board
更糟糕 - 您正在复制can_put_on_board
实施的代码并使接口混乱。
与所有代码气味一样,它们应该让你警惕,并且它们并不一定意味着你的代码是坏的。这一切都取决于你想要实现的目标。
如果您正在实施一款拥有5k行代码,两类卡片的棋盘游戏,那么任何可行的游戏都可以。如果您正在设计更大,可扩展且可能允许由非程序员创建的卡片(无论是实际需要还是您正在进行研究),那么这可能会赢得'做。
假设后者,让我们看一些替代方案。
您可以将卡的适当责任放在卡上,而不是一些外部代码。例如。向卡片添加dynamic_cast
功能(play(Context& c)
是访问电路板的方法,可能需要任何其他功能)。董事会卡会知道它可能只适用于董事会而且不需要演员。
但我完全放弃使用继承。它的许多问题之一是它如何引入所有卡的分类。让我举个例子:
Context
和BoardCard
将所有卡片放入这两个存储桶中; ActionCard
或Action
卡; Board
类型或任何不同的方式); BoardActionCard
,RedBoardCard
,BlueBoardCard
等?为什么应该避免继承以及如何实现运行时多态性的其他示例,否则您可能希望观看Sean Parent的优秀"Inheritance is the Base Class of Evil" talk。实现这种多态性的有前途的库是dyno,但我还没有尝试过。
可能的解决方案可能是:
RedActionCard
然后您可以为每种卡类型创建类:
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_;
};
或实施不同类别的行为,例如有一个
class Goblin final {
void play(Context& c) const {
// apply effects of card, e.g. take c.board() and put the card there
}
};
模板然后使用enable_if来处理不同的类别:
template <class T>
void play(const T& card, Context& c);
其中:
template <class T, class = std::enable_if<IsBoardCard_v<T>>
void play(const T& card, Context& c) {
c.board().add(Card(card));
}
然后将您的template <class T>
struct IsBoardCard {
static constexpr auto value = T::IS_BOARD_CARD;
};
template <class T>
using IsBoardCard_v = IsBoardCard<T>::value;
定义为:
Goblin
允许您在多个维度对卡片进行分类,也可以通过实现不同的class Goblin final {
public:
static constexpr auto IS_BOARD_CARD = true;
static constexpr auto COLOR = Color::RED;
static constexpr auto SUPERMAGIC = true;
};
功能来完全专门化行为。
示例代码使用std :: shared_ptr来存储模型,但你绝对可以在这里做更聪明的事情。我喜欢使用静态大小的存储空间,只允许使用特定最大尺寸和对齐的Ts。或者,你可以使用std :: unique_ptr(虽然会禁用复制)或利用小尺寸优化的变体。
答案 1 :(得分:3)
您可以应用Microsoft's COM背后的原则并提供一系列接口,每个接口都描述一组相关行为。在COM中,您可以通过调用QueryInterface
来确定特定接口是否可用,但在现代C ++中,dynamic_cast
的工作方式类似且效率更高。
class Card {
virtual void ~Card() {} // must have at least one virtual method for dynamic_cast
};
struct IBoardCard {
virtual void put_card(Board* board);
};
class BoardCard : public Card, public IBoardCard {};
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(Card const* card) {
const IBoardCard *p = dynamic_cast<const IBoardCard*>(card);
if (p != null) p->put_card(this);
};
这可能是一个不好的例子,但我希望你明白这一点。
答案 2 :(得分:3)
dynamic_cast
dynamic_cast
通常不受欢迎,因为它很容易被滥用以完全破坏所使用的抽象。依赖于具体实施并不明智。当然它可能需要,但很少,所以几乎每个人都有一个经验法则 - 可能你不应该使用它。它的代码味道可能意味着您应该重新考虑您的抽象,因为它们可能不是您域中所需的。也许在你的游戏中,Board
不应该有put_card
方法 - 也许卡应该有方法play(const PlaySpace *)
,其中Board
实现PlaySpace
或类似的东西。甚至是CppCoreGuidelines discourage using dynamic_cast
in most cases。
一般来说,很少有人会遇到这样的问题,但我已经多次遇到过它。该问题称为 Double(或Multiple)Dispatch 。这里很古老,但关于双重调度的相关文章(介意史前auto_ptr
):
http://www.drdobbs.com/double-dispatch-revisited/184405527
Scott Meyers在他的一本书中写了一些关于使用dynamic_cast
构建双调度矩阵的内容。但是,总而言之,这些dynamic_cast
被隐藏在这个矩阵中 - 用户不知道里面发生了什么样的魔法。
值得注意的是 - 多次调度也被认为是代码味道: - )。
查看visitor pattern。它可以用作dynamic_cast
的替代品,但它也是某种代码味道。
我通常建议使用dynamic_cast
和visitor作为设计问题的最后手段工具,因为它们会破坏抽象,从而增加复杂性。
答案 3 :(得分:2)
在我看来,两种类型的卡是完全不同的。董事会卡和行动卡可以做的事情是相互排斥的,而且常见的是他们可以从套牌中抽出。此外,这不是卡所做的事情,而是玩家/套牌行动。
如果这是真的,问题应该是是否真的应该来自普通类型,Card
。另一种设计是标记的联合:让Card
代替std::variant<BoardCard, ActionCard...>
,包含相应类型的实例。在决定如何处理该卡时,您使用switch
上的index()
,然后std::get<>
使用相应的类型。通过这种方式,您不需要任何 *_cast
运算符,并且可以完全自由地使用哪种类型的卡支持哪些方法(对其他类型都没有意义)。
如果它只是几乎是真的,但不适用于所有类型,你可以稍微变化:只将那些可以理智地超级分类的卡组合在一起,并将这些常见类型的集合放入{ {1}}。
答案 4 :(得分:1)
我总是发现一个代码气味的使用,根据我的经验,90%的时间演员是由于糟糕的设计。 我在一些时间关键的应用程序中看到了dynamic_cast的用法,它提供了比从多个接口继承或从对象中检索某种枚举(如类型)更多的性能改进。所以代码闻名,但在这种情况下动态转换的使用是值得的。
那就是说,我会避免你的情况下的动态转换以及来自不同接口的多重继承。
在我的解决方案之前,你的描述听起来有很多关于卡的行为或它们在板上的后果的细节。 和游戏本身。我用它作为进一步的约束,试图保持盒子和可维护的东西。
我会选择合成而不是继承。它将为您提供将卡片用作“工厂”的机会:
有关详细信息,请参阅[https://en.wikipedia.org/wiki/Composition_over_inheritance]。我想引用一下: 组合还提供了一个更长期稳定的业务领域,因为它不太容易出现家庭成员的怪癖。换句话说,最好是组合一个对象可以做什么(HAS-A)而不是扩展它的内容( IS - A)。[1]
BoardCard / Element可以是这样的:
//the card placed on the board.
class BoardElement {
public:
BoardElement() {}
virtual ~BoardElement() {};
//up to you if you want to add a read() methods to read data from the card description (XML / JSON / binary data)
// but that should not be part of the interface. Talking about a potential "Wizard", it's probably more related to the WizardCard - WizardElement relation/implementation
//some helpful methods:
// to be called by the board when placed
virtual void OnBoard() {}
virtual void Frame(const float time) { /*do something time based*/ }
virtual void Draw() {}
// to be called by the board when removed
virtual void RemovedFromBoard() {}
};
卡可以代表在牌组或用户手中使用的东西,我会添加这种界面
class Card {
public:
Card() {}
virtual ~Card() {}
//that will be invoked by the user in order to provide something to the Board, or NULL if nothing should be added.
virtual std::shared_ptr<BoardElement*> getBoardElement() { return nullptr; }
virtual void Frame(const float time) { /*do something time based*/ }
virtual void Draw() {}
//usefull to handle resources or internal states
virtual void OnUserHands() {}
virtual void Dropped() {}
};
我想补充一点,这种模式允许在getBoardElement()
方法中使用许多技巧,从而充当工厂(所以应该用它自己的生命周期产生某些东西),
返回Card
数据成员,例如std:shared_ptr<BoardElement> wizard3D;
(例如),在Card
和BoardElement
之间创建绑定,如下所示:
class WizardBoardElement : public BoardElement {
public:
WizardBoardElement(const Card* owner);
// other members omitted ...
};
绑定对于读取某些配置数据或其他任何内容非常有用......
因此,Card
和BoardElement
的继承将用于实现基类公开的功能,而不是用于提供只能通过dynamic_cast
访问的其他方法。
为了完整性:
class Player {
void add(Card* card) {
//..
card->OnUserHands();
//..
}
void useCard(Card* card) {
//..
//someway he's got to retrieve the board...
getBoard()->add(card->getBoardElement());
//..
}
Card const* get_card();
};
class Board {
void add(BoardElement* el) {
//..
el->OnBoard();
//..
}
};
通过这种方式,我们没有dynamic_cast,玩家和董事会在不知道处理卡的内部细节的情况下做了简单的事情,在不同对象之间提供了良好的分离并提高了可维护性。
谈论可能适用于其他玩家或您的头像的ActionCard
和“效果”,我们可以考虑采用以下方法:
enum EffectTarget {
MySelf, //a player on itself, an enemy on itself
MainPlayer,
Opponents,
StrongOpponents
//....
};
class Effect {
public:
//...
virtual void Do(Target* target) = 0;
//...
};
class Card {
public:
//...
struct Modifiers {
EffectTarget eTarget;
std::shared_ptr<Effect> effect;
};
virtual std::vector<Modifiers> getModifiers() { /*...*/ }
//...
};
class Player : public Target {
public:
void useCard(Card* card) {
//..
//someway he's got to retrieve the board...
getBoard()->add(card->getBoardElement());
auto modifiers = card->getModifiers();
for each (auto modifier in modifiers)
{
//this method is supposed to look at the board, at the player and retrieve the instance of the target
Target* target = getTarget(modifier.eTarget);
modifier.effect->Do(target);
}
//..
}
};
这是应用卡片效果的相同模式的另一个例子,避免卡片知道有关电路板的详细信息及其状态,谁在播放卡片,并使代码保持在Player
非常简单。
希望这可能有所帮助, 祝你今天愉快, 斯特凡诺。
答案 5 :(得分: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; };
};
通过这种方式,您可以简单地使用ICard
的引用或指针,并检查它所包含的实际类型是否可以放在Board
上。
但我找不到任何具有适当用法的文章(显示出良好的设计,而不仅仅是&#34;如何使用&#34;)。
一般来说,我说动态演员没有任何好的,真实的用例。
有时我在调试代码中使用它来进行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
}
};
答案 6 :(得分:0)
我认为我最终会得到类似的结果(用clang 5.0编译,带-std = c ++ 17)。我对你的评论很好。因此,每当我想处理不同类型的卡时,我需要实例化一个调度程序并提供具有适当签名的方法。
#include <iostream>
#include <typeinfo>
#include <type_traits>
#include <vector>
template <class T, class... Args>
struct any_abstract {
static bool constexpr value = std::is_abstract<T>::value || any_abstract<Args...>::value;
};
template <class T>
struct any_abstract<T> {
static bool constexpr value = std::is_abstract<T>::value;
};
template <class T, class... Args>
struct StaticDispatcherImpl {
template <class P, class U>
static void dispatch(P* ptr, U* object) {
if (typeid(*object) == typeid(T)) {
ptr->do_dispatch(*static_cast<T*>(object));
return;
}
if constexpr (sizeof...(Args)) {
StaticDispatcherImpl<Args...>::dispatch(ptr, object);
}
}
};
template <class Derived, class... Args>
struct StaticDispatcher {
static_assert(not any_abstract<Args...>::value);
template <class U>
void dispatch(U* object) {
if (object) {
StaticDispatcherImpl<Args...>::dispatch(static_cast<Derived *>(this), object);
}
}
};
struct Card {
virtual ~Card() {}
};
struct BoardCard : Card {};
struct ActionCard : Card {};
struct Board {
void put_card(BoardCard const& card, int const row, int const column) {
std::cout << "Putting card on " << row << " " << column << std::endl;
}
};
struct UI : StaticDispatcher<UI, BoardCard, ActionCard> {
void do_dispatch(BoardCard const& card) {
std::cout << "Get row to put: ";
int row;
std::cin >> row;
std::cout << "Get row to put:";
int column;
std::cin >> column;
board.put_card(card, row, column);
}
void do_dispatch(ActionCard& card) {
std::cout << "Handling action card" << std::endl;
}
private:
Board board;
};
struct Game {};
int main(int, char**) {
Card* card;
ActionCard ac;
BoardCard bc;
UI ui;
card = ∾
ui.dispatch(card);
card = &bc;
ui.dispatch(card);
return 0;
}
答案 7 :(得分:0)
我无法理解为什么你不会使用虚拟方法,我只会介绍,我将如何做。首先,我有所有卡的ICard
界面。然后我会区分卡片类型(即BoardCard和ActionCard以及你拥有的任何卡片)。并且所有卡都从其中一种卡类型继承。
class ICard {
virtual void put_card(Board* board) = 0;
virtual void accept(CardVisitor& visitor) = 0; // See later, visitor pattern
}
class ActionCard : public ICard {
void put_card(Board* board) final {
// std::cout << "You can't put Action Cards on the board << std::endl;
// Or just do nothing, if the decision of putting the card on the board
// is not up to the user
}
}
class BoardCard : public ICard {
void put_card(Board* board) final {
// Whatever implementation puts the card on the board, mb something like:
board->place_card_on_board(this);
}
}
class SomeBoardCard : public BoardCard {
void accept(CardVisitor& visitor) final { // visitor pattern
visitor.visit(this);
}
void print_information(); // see BaseCardVisitor in the next code section
}
class SomeActionCard : public ActionCard {
void accept(CardVisitor& visitor) final { // visitor pattern
visitor.visit(this);
}
void print_information(); // see BaseCardVisitor
}
class Board {
void put_card(ICard* const card) {
card->put_card(this);
}
void place_card_on_board(BoardCard* card) {
// place it on the board
}
}
我想用户必须知道他画了什么牌,所以为此我会实现访客模式。您还可以将accept
- 方法放置在卡片类型(BoardCard,ActionCard)中,放置在最派生的类/卡片中,依赖于您想要绘制线条的位置,该线条应提供给哪些信息。用户。
template <class T>
class BaseCardVisitor {
void visit(T* card) {
card->print_information();
}
}
class CardVisitor : public BaseCardVisitor<SomeBoardCard>,
public BaseCardVisitor<SomeActionCard> {
}
class Player {
void add_card(ICard* card);
ICard const* get_card();
void what_is_this_card(ICard* card) {
card->accept(visitor);
}
private:
CardVisitor visitor;
};
答案 8 :(得分:0)
几乎没有一个完整的答案,但只想提出一个类似于Mark Ransom
的答案,但只是一般来说,我发现向下转换对于鸭子打字是有用的真有用。可以有某些架构,这样做是非常有用的:
for each object in scene:
{
if object can fly:
make object fly
}
或者:
for each object in scene that can fly:
make object fly
COM允许这种类似的东西:
for each object in scene:
{
// Request to retrieve a flyable interface from
// the object.
IFlyable* flyable = object.query_interface<IFlyable>();
// If the object provides such an interface, make
// it fly.
if (flyable)
flyable->fly();
}
或者:
for each flyable in scene.query<IFlyable>:
flyable->fly();
这意味着在集中式代码中某处使用某种形式的强制转换来查询和获取接口(例如:从IUnknown
到IFlyable
)。在这种情况下,动态转换检查运行时类型信息是最安全的转换类型。首先,可能会进行一般性检查以查看对象是否提供了不涉及强制转换的界面。如果它没有,则此query_interface
函数可能返回空指针或某种类型的空句柄/引用。如果是这样,那么对RTTI使用dynamic_cast
是最安全的做法,可以获取指向通用接口的实际指针(例如:IInterface*
)并将IFlyable*
返回给客户端。 / p>
另一个例子是实体组件系统。在这种情况下,我们不是查询抽象接口,而是检索具体组件(数据):
Flight System:
for each object in scene:
{
if object.has<Wings>():
make object fly using object.get<Wings>()
}
或者:
for each wings in scene.query<Wings>()
make wings fly
......出现这种情况,这也意味着在某个地方施展。
对于我的域名(VFX,在应用程序和场景状态方面有点类似于游戏),我发现这种类型的ECS架构是最容易维护的。我只能说自己的经验,但我已经存在了很长时间,面对许多不同的架构。 COM现在是VFX中最流行的架构风格,我曾经在广泛用于电影和游戏以及archviz等广泛使用COM架构的商业VFX应用程序,但我发现ECS在游戏中很受欢迎对于我的特殊情况,引擎甚至比COM更容易维护。
- 我发现ECS更容易的原因之一是因为此域中的大部分系统(如
PhysicsSystem
,RenderingSystem
,AnimationSystem
等等都归结为数据转换器并且ECS模型恰好适合于此目的而没有抽象阻碍。使用此域中的COM,实现类似IMotion
等动作接口的接口的子类型数量可能为数百个(例如:PointLight
实现IMotion
以及其他5个接口) ,需要数百个类实现COM接口的不同组合才能单独维护。使用ECS,它使用组合模型而不是继承,并将数百个类减少到只有几十个简单组件structs
,这些组件可以由组成它们的实体以无限的方式组合,并且只有少数几个系统必须提供行为:其他一切都只是系统作为输入循环的数据,然后提供一些输出。
在我的特定域的可维护性方面,使用一堆全局变量和暴力编码的遗留代码库(例如:遍布各处的条件而不是使用多态),深层继承层次结构,COM和ECS,我说ECS > COM
,而深度继承层次结构和全局变量的暴力编码都非常难以维护(使用受保护数据字段的深度继承的OOP几乎同样难以理解)将不变量作为一大堆全局变量IMO保持不变的条款,但如果设计需要改变,那么进一步可以引起跨越整个层次结构的最噩梦级联变化 - 至少蛮力遗留代码库没有产生级联问题,因为它几乎没有重用任何代码开始)。
IFlyable
)。使用ECS,依赖关系流向中央数据(由ECS实体提供的组件,如Wings
)。两者的核心通常是我们有一堆非同类对象(或者#34;实体&#34;),它们提供的接口或组件事先是未知的,因为我们正在访问他们通过一个非同类的集合(例如:&#34;场景&#34;)。因此,我们需要在运行时通过查询集合或单独的对象来查看它们提供的内容,从而在运行时发现它们的功能。
无论哪种方式,都涉及某种类型的集中式转换来从实体中检索接口或组件,如果我们必须向下转换,那么dynamic_cast
至少是最安全的方法运行时类型检查以确保转换有效。对于ECS和COM,通常只需要在整个系统中执行一行代码来执行此演员。
也就是说,运行时检查的成本很低。通常,如果在COM和ECS体系结构中使用dynamic_cast
,那么它的完成方式应该永远不会抛出std::bad_cast
和/或dynamic_cast
本身永远不会返回nullptr
dynamic_cast
1}}(PosAndVelocity
只是一个健全性检查,以确保没有内部程序员错误,而不是一种确定对象是否继承类型的方法)。进行另一种类型的运行时检查以避免这种情况(例如:在获取所有PosAndVelocity
组件时,ECS中的整个查询只需执行一次,以确定要使用哪个组件列表实际上是同类的并且只存储template<class To, class From> To checked_cast(From* from) {
assert( dynamic_cast<To>(from) == static_cast<To>(from) && "checked_cast failed" );
return static_cast<To>(from);
}
template<class To, class From> To checked_cast(From& from) {
assert( dynamic_cast<To>(from) == static_cast<To>(from) && "checked_cast failed" );
return static_cast<To>(from);
}
组件)。如果这个小的运行时成本是不可忽视的,因为你在每一帧上循环遍历大量的组件并对每个组件做了琐碎的工作,那么我发现这个片段在Herb Sutter的C ++编码标准中很有用:
dynamic_cast
它基本上使用assert
作为调试版本的健全性检查,其中static_cast
和{{1}}用于版本构建。