【问题标题】:Component/Strategy Pattern in C++, Design and ImplementationC++ 中的组件/策略模式,设计和实现
【发布时间】:2020-01-19 04:10:23
【问题描述】:

我有一个 C++ 项目,使用 OpenFrameworks 进行渲染,它在窗口中反弹了几个球(圆圈)。它们是使用组件模式(有时称为策略模式)实现的,以分别封装外观和行为。这使得外观(此处:Amiga Ball)和行为(此处:重力弹跳)在运行时可互换。

模式本身的灵感来自Robert Nystrom's book Game Programming Patterns 中的组件模式章节。 在按下一个键时,“重力弹跳”的行为与“无重力弹跳”交换(这可能是我在编程而不是设计的原因)。 我想使用智能指针来实现该模式(在这种情况下,unique_ptr 会这样做,因为没有共享,并且一个组件对象始终由 GameObject 唯一持有)。 我的代码中有几个“丑陋”的部分与 unique_ptr 的使用以及我对组件使用多态性但似乎有时不得不将它们视为派生类对象而不是基类的事实有关。

我想知道它是否是一个好的和可行的设计和实施 w.r.t.该模式的总体设计和实现,尤其是智能指针的使用。

注意:我不关心这里的 const 正确性,尽管我应该关心。所以我敢肯定有很多 const 可以放在那里,但请不要仅仅因为这个而抨击我,除非它会影响其他问题。

所以这里...

我们有一个 GameObject 类,其中包含屏幕上任何对象应具有的最少信息: GameObject.hpp:

    class GameObject {
    public:
        float x;
        float y;

        GameObject(unique_ptr<PhysicsComponent> physics, unique_ptr<GraphicsComponent> graphics);
        GameObject();
        GameObject(float x, float y);

        void update();
        void draw();
        void setPhysicsComponent(unique_ptr<PhysicsComponent> pc);
        void setGraphicsComponent(unique_ptr<GraphicsComponent> pc);
        unique_ptr<PhysicsComponent>& getPhysicsComponent();
        unique_ptr<GraphicsComponent>& getGraphicsComponent();

    private:
        unique_ptr<PhysicsComponent> physics_;
        unique_ptr<GraphicsComponent> graphics_; 
    };

GameObject.cpp:

GameObject::GameObject(unique_ptr<PhysicsComponent> physics, unique_ptr<GraphicsComponent> graphics) : physics_(move(physics)), graphics_(move(graphics)) {}

GameObject::GameObject() {}

GameObject::GameObject(float x, float y) : x(x), y(y) {}

void GameObject::update(){
    physics_->update(*this);
}

void GameObject::draw() {
    graphics_->draw(*this);
}

void GameObject::setPhysicsComponent(unique_ptr<PhysicsComponent> pc) {
    physics_ = std::move(pc);
}

void GameObject::setGraphicsComponent(unique_ptr<GraphicsComponent> gc) {
    graphics_ = std::move(gc);
}

std::unique_ptr<GraphicsComponent>& GameObject::getGraphicsComponent() {
    return graphics_;
}

std::unique_ptr<PhysicsComponent>& GameObject::getPhysicsComponent() {
    return physics_;
}

然后是组件的基类(在这种情况下,让我们关注物理组件,因为图形组件是等价的):

class GameObject;

class PhysicsComponent {
    public:
        virtual ~PhysicsComponent() {std::cout<<"~PhysicsComponent()"<<std::endl;}
        virtual void update(GameObject& obj) = 0;

        void setXSpeed(float xs) {xSpeed = xs;};
        float getXSpeed() {return xSpeed;};
        void setYSpeed(float ys) {ySpeed = ys;};
        float getYSpeed() {return ySpeed;};

    protected:
        float ySpeed;
        float xSpeed;
};

PhysicsComponent 的具体实现如下所示:

class GravityBouncePhysicsComponent : public PhysicsComponent {
    public:
        virtual ~GravityBouncePhysicsComponent()  {std::cout<<"~GravityBouncePhysicsComponent()"<<std::endl;};
        GravityBouncePhysicsComponent();
        GravityBouncePhysicsComponent(float radius, float xSpeed, float ySpeed, float yAccel);
        virtual void update(GameObject& obj);
        void setRadius(float r) {radius = r;};
        float getRadius() {return radius;};
    private:
        // Physics
        float yAcceleration;
        float radius;
};

在 GravityBouncePhysicsComponent.cpp 中,空构造函数创建了这样一个组件,其中 xSpeed 和 ySpeed(位于基类中)和 yAcceleration(仅在派生组件类中有意义)的随机值。 然后还有一个叫做 FloatBouncePhysicsComponent 的代码类似。

现在我想在主程序中使用这些类,恰好是 OpenFrameworks 中的 ofApp 类。头文件是这样的(省略了一些无关紧要的东西):

class ofApp : public ofBaseApp{
    public:
        void setup();
        void update();
        void draw();

        void keyPressed(int key);
        // ... lots of other event functions for mouse etc.

    private:
        std::vector<GameObject> balls;
        GameObject createAmigaBall(float x, float y);

        // more variables and some methods here ...

};

现在在 ofApp.cpp 文件中它开始变得丑陋。例如,在创建新球的辅助函数中,它看起来像这样:

GameObject ofApp::createAmigaBall(float x, float y) {
    GameObject go(x,y); // create an empty GameObject on the stack at position x,y
    go.setGraphicsComponent(std::make_unique<AmigaBallGraphicsComponent>());

    if(gravity) {
        go.setPhysicsComponent(std::make_unique<GravityBouncePhysicsComponent>());
        float r = (static_cast<AmigaBallGraphicsComponent*>((go.getGraphicsComponent()).get()))->getRadius();
        (static_cast<GravityBouncePhysicsComponent*>((go.getPhysicsComponent()).get()))->setRadius(r);
    } else {
        go.setPhysicsComponent(std::make_unique<FloatBouncePhysicsComponent>());
        float r = (static_cast<AmigaBallGraphicsComponent*>((go.getGraphicsComponent()).get()))->getRadius();
        (static_cast<FloatBouncePhysicsComponent*>((go.getPhysicsComponent()).get()))->setRadius(r);
    }

    return go; // calls the copy constructor for return value, so we have no dangling pointers or refs
}

这里 AmigaBallGraphicsComponent 的空构造函数生成一个随机半径。这个半径属于这个派生类,因为它肯定不在每个 GraphicsComponent 中。 然而,这导致不得不笨拙地从生成的具有丑陋强制转换的组件中提取半径并访问原始指针,只是为了在 GravityBouncePhysicsComponent 中设置相同的半径而再次这样做。

如果你觉得这很丑,看看这个 sn-p:

void ofApp::keyPressed(int key){
    if(key == 'a') {
        if(gravity) {
            for(int i=0; i<balls.size(); i++) {
                unique_ptr<GravityBouncePhysicsComponent> opc (static_cast<GravityBouncePhysicsComponent*>((balls[i].getPhysicsComponent()).release()));
                float r = opc->getRadius();
                float xs = opc->getXSpeed();
                float ys = opc->getYSpeed();
                balls[i].setPhysicsComponent(std::make_unique<FloatBouncePhysicsComponent>(r,xs,ys));
            }
        } else {
                // similar code for the non-gravity case omitted ...
            }
        }
        gravity =!gravity;
    }
}

而且,好像强制转换为派生类的东西还不够,结果证明这段代码不起作用

auto gc = std::make_unique<AmigaBallGraphicsComponent>();
go.setGraphicsComponent(gc);

而这个。什么...?

go.setGraphicsComponent(std::make_unique<AmigaBallGraphicsComponent>());

这不应该完全一样吗?

提前感谢您对整个混乱情况的任何见解(或者是吗?)。

【问题讨论】:

  • 请发布您的编译器向您报告的实际错误。
  • go.setGraphicsComponent(std::move(gc));std::unique_ptr 的全部意义在于它只能移动以确保唯一的所有权。
  • 没有错误。该代码有效。我的问题清楚地询问该模式的一般设计和实现是否在 C++ 中做得很好,以及我使用智能指针是否可行。
  • 在 getter 中返回对 unique_ptr 的引用没有多大意义。如果 getter 的目的是授予对对象的读写访问权限,同时保留对象的所有权,则只需返回一个引用。
  • @Martin 我正在回答你最底层的问题:你最后两个 sn-ps 相同,第一个尝试复制 std::unique_ptr 而不是移动它,这就是它不起作用的原因。

标签: c++ polymorphism smart-pointers unique-ptr strategy-pattern


【解决方案1】:

我认为您的大多数问题都来自这样一个事实,即您让所有权 (std::unique_ptr) 和类型擦除(多态性)问题从您的 API 中流出,这需要大量不相关的生命周期管理和对用户端。 getGraphicsComponent(及其伙伴)可以这样实现:

template <class DerivedGraphicsComponent = GraphicsComponent>
auto &getGraphicsComponent() {
    return static_cast<DerivedGraphicsComponent &>(*graphics_);
}

这使您可以编写:

float r = go.getGraphicsComponent<AmigaBallGraphicsComponent>().getRadius();

或者省略&lt;AmigaBallGraphicsComponent&gt; 并接收一个普通的GraphicsComponent &amp;

您确实失去了GameObject 撤销其组件之一的所有权并通过std::unique_ptr 将其归还的能力,但看起来您实际上并不需要它。您使用该功能的 sn-p 最终会执行以下操作:

balls[i].setPhysicsComponent(std::make_unique<FloatBouncePhysicsComponent>(r,xs,ys));

... 无论如何都会通过graphics_ 的移动赋值运算符破坏前一个组件。如果您确实需要该功能,请务必添加另一个功能:

template <class DerivedGraphicsComponent = GraphicsComponent>
auto releaseGraphicsComponent() {
    return std::unique_ptr<DerivedGraphicsComponent>(
        static_cast<DerivedGraphicsComponent *>(graphics_.release())
    );
}

【讨论】:

  • 嘿,那里有相当先进的技术......几乎就像......魔术! :P。非常感谢人!这使代码更加优雅并杀死了所有类型转换(很好地将类型决定转移到模板参数)。好东西。我确实需要切换到 C++14(在 OpenFrameworks 项目中不容易找到),但这成功了。永远不会想到模板函数,尽管 C# 中一直在使用这样的东西。我猜隐藏在明显的视线中。
  • @Martin 在我们处理模板时,我建议您在调试版本中将 static_cast 替换为 dynamic_cast 以捕获向下转换错误——否则它们只会触发 UB。写得干净利落作为练习:)
  • @Martin 如果你写了一个完全不可能的演员表,它确实会编译失败。向下转换到给定派生类将编译,但在运行时,该基类引用背后的实际对象很可能属于另一个派生类 (BouncingHotdogGraphicsComponent),在这种情况下转换无效。
  • dynamic_cast 对此进行检查并将抛出异常(或在其指针变体中返回 nullptr),而 static_cast 不会检查任何内容并愉快地转换为错误的类型,从而触发 Undefined行为。 UB 意味着,正式地,您将离开其标准定义的功能良好的 C++ 领域,并且非正式地,您的程序将以各种(有时是可笑的荒谬)方式发生故障,因为它现在正在执行没有意义的代码(在这种情况下,操作一个实际上并不存在的对象)。
  • 权衡是static_cast 尽可能快,但不进行任何运行时检查(因此是“静态”,即编译时),而dynamic_cast 将捕获并报告您的错误以明确定义的方式,但需要运行时机器来执行此操作(即,类必须是多态的,并且强制转换本身有成本)。编辑:分段错误确实是 UB 的可能结果,但它也可能破坏内存,以荒谬的方式执行代码,甚至在你以错误的方式轻推它之前似乎工作正常。没有保证:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-09-15
  • 1970-01-01
  • 2012-03-21
  • 1970-01-01
  • 2010-09-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多