C++模板类和嵌套类

3
我有一个问题,涉及到typename SnakeGame。我想知道如何将SnakeGame作为全局类型放到KeyboardEvents类中。现在像DirectionKeyboard这样的嵌套类不知道SnakeGame是什么类型,因为它只能看到KeyboardEvents<SnakeGame>类型。我不知道该怎么改:P

以下是错误信息:

无法将参数1从“KeyboardEvents SnakeGame>&”转换为“SnakeGame&”

我真的很需要帮助。

keyboardEvents.hpp

#include<SFML/Graphics.hpp>

template <typename SnakeGame>
class KeyboardEvents {
public:
    virtual ~KeyboardEvents() = default;

protected:
    class DirectionKeyboardEvent{
    public:
        virtual ~DirectionKeyboardEvent() = default;
        virtual void direction(SnakeGame&) = 0; // error no know conversion 
    };      

    class GoRight : public DirectionKeyboardEvent {
    public:
        void direction(SnakeGame& snakeObj) {
            snakeObj.snake[0].xCoor+=1;
        }
    };

    class GoRight : public DirectionKeyboardEvent {
    public:
        void direction(SnakeGame& snakeObj){
            snakeObj.snake[0].xCoor += 1;
        }
    };

    class GoLeft : public DirectionKeyboardEvent{
    public:
        void direction(SnakeGame& snakeObj){
            snakeObj.snake[0].xCoor-=1;
        }                     
    };                            

    class GoUp:public DirectionKeyboardEvent{
    public:
        void direction(SnakeGame& snakeObj){
            snakeObj.snake[0].yCoor-=1;
        }                   
    };

    class GoDown : public DirectionKeyboardEvent{
    public:
        void direction(SnakeGame& snakeObj){
            snakeObj.snake[0].yCoor+=1;
        }
    };

    std::map<sf::Keyboard::Key,  std::shared_ptr<DirectionKeyboardEvent>> mapOfDirects;

    void initializeDirectionMap() {
        mapOfDirects[sf::Keyboard::Right] = std::shared_ptr< DirectionKeyboardEvent >(new GoRight);
        mapOfDirects[sf::Keyboard::Left] = std::shared_ptr<DirectionKeyboardEvent>(new GoLeft);
        mapOfDirects[sf::Keyboard::Up] = std::shared_ptr<DirectionKeyboardEvent>(new GoUp);
        mapOfDirects[sf::Keyboard::Down] = std::shared_ptr<DirectionKeyboardEvent>(new GoDown);
    }

    void chooseMethodFromKeyboardArrows(sf::Keyboard::Key codeFromKeyboard) {
        auto iterator = mapOfDirects.find(codeFromKeyboard);

        if(iterator!=mapOfDirects.end()){
            iterator->second->direction(*this);//left , right,up , down, pause
            mainDirection=codeFromKeyboard;
        } else {
            mapOfDirects[mainDirection]->direction(*this);
        }
    }
};

这里是我使用KeyboardEvents的类——snakeGame.hpp。
#include"keyboardEvents.hpp"

class SnakeGame:public Screen, public KeyboardEvents<SnakeGame> {
    public:
        SnakeGame(int size=16, int width=15, int height=15, int timeDelay=60000)
            : Screen(size, width, height), KeyboardEvents<SnakeGame>(), timeDelay(timeDelay) {}
};

我没有其他想法 :P 我之前把 SnakeGameKeyboardEvents 写在同一个文件里,但是现在我想重构这段代码,所以我把 KeyboardEvents 移到了 keyboardEvents.hpp 文件中 ;)。 - Klemens
3
似乎你试图过度概括你的设计。如果KeyboardEvents类直接在SnakeGame的成员中探测,你将无法使用除SnakeGame之外的任何类型,这使得这个案例不适合使用模板。 - Peter
你也过度使用了继承。一个“游戏”从屏幕对象继承毫无意义。游戏可能包含一个屏幕对象。 - Peter
@Peter 我完全同意你的看法,但我没有其他想法;/ 最近我开始学习面向对象编程和C++,但我经常犯一些愚蠢的错误。对此我感到抱歉。我很乐意接受批评和建议:P - Klemens
@Klemens 顺便说一下,你不必在每个地方都加上虚拟析构函数。只需在接口和抽象类中添加它们,其他超类可能不需要。至少,可以使用 virtual ~MyClass() = default 替代 virtual ~MyClass(){} - Guillaume Racicot
显示剩余6条评论
2个回答

3
您的设计似乎有点过于复杂。我认为这可能是因为您一边设计一边进行的缘故。有时候,如果必要的话,在白板上画出盒子和线条并静思片刻会有所帮助。
总之,这不是对您问题的直接回答,而是基于我猜测您尝试做的事情的替代方案建议。
在我看来,您正试图实现一个通用的键盘输入处理程序并将其与游戏绑定。虽然我可能完全错误,但如果没有,可以考虑使用类似以下内容的东西。首先,为接收键盘事件的对象定义一个通用接口。它不必是一个模板,这并不是一个很好的模板应用场景:
class KeyboardEventHandler {
public:
    enum Direction { Left, Right, Up, Down };
    virtual ~KeyboardEventHandler () { }
    virtual void onDirectionKey (Direction d) = 0;
};

现在你的SnakeGame,它处理键盘事件,可以继承并实现自己特定于SnakeGame的逻辑:

class SnakeGame : public KeyboardEventHandler {
public:
    void onDirectionKey (Direction d) {
        switch (d) {
        case Up: ...
        case Down: ...
        case Left: ...
        case Right: ...
        }
    }
};

然后,任何处理键盘事件并驱动所有这些的代码都可以使用KeyboardEventHandler *,它可以是SnakeGame,也可以是您将来决定使用的任何其他东西。

这只是一种组织方式。例如,您可以改为按以下方式结构化,拆分KeyboardEvent,这可能会简化未来的添加:

class KeyboardEvent {
public:
    enum Direction { Left, Right, Up, Down };
    Direction getDirection () { ... } // or whatever
};

class KeyboardEventHandler {
public:
    virtual ~KeyboardEventHandler () { }
    virtual void onEvent (KeyboardEvent &event) = 0;
};

SnakeGame 定义为:

class SnakeGame : public KeyboardEventHandler {
public:
    void onEvent (KeyboardEvent &event) {
        ...
    }
};

如果需要的话,您可以将那些东西命名为除了“Direction” / “onDirectionKey”之外的其他名称。我从您的示例中选择了这个名称,但是请确保它在语义上合适且方便(例如,如果您计划将其扩展以包括更多内容)。但是不管怎样,这都不是重点。
还有许多其他方法可以解决这个问题,但重要的是:如果您正在尝试为某些东西创建通用接口,则实际上不能使其依赖于继承它的具体细节,否则您就会失去最初创建通用接口的目的。在这种情况下,要么它不适合使用通用基础/继承,要么您设计出了瑕疵,需要停下来重新思考。
记住:您的目标不是在代码中添加尽可能多的类和其他内容;您不是在追求继承高分。您的目标是保持代码清晰、可读、可维护、正确、可能可重用,并使自己的工作更轻松。这些都是工具,不要仅仅因为您拥有它们而使用它们,而是在需要时使用它们使您的生活更轻松。
然而,所有这些都是针对您特定应用程序的过度处理,虽然这是一个有趣的练习。老实说,在您特定的情况下,我会完全放弃所有继承和类似的内容,并采取类似于以下的做法:
class SnakeGame {
public:
    void handleKeyPress (char c) {
        // ... do the right thing here
    }
}

并且完成它。

非常感谢!我已经做了KeyboardEvents等等,因为我开始学习oop并且想在贪吃蛇游戏中训练一些方法。例如,我摆脱了ifsswitchs,并将所有语句转移到类中。当我在SnakeGame类中有KeyboardEvents时,一切都很好。但是当我的程序变得越来越大时,我决定将KeyboardEvents类移到其他文件中,然后我就遇到了这个问题。我知道这有点过度设计,但我只想练习oop的方法:P但现在我知道我的方法不太好:P - Klemens
请注意,“OOP”并不意味着完全没有“if”和“switch”语句! OOP概念旨在使设计、实现和重用更加容易,而不是更难,因此,如果您发现事情变得混乱不堪,那么就是时候放松一下,重新审视一下了。通常,暂时远离代码并什么都不做会有所帮助;您的大脑通常会在背景中想出一些好的东西。此外,顺便提一下,准确地为您的对象命名有时会有所帮助:在高层次上:它是什么/它做什么/它为什么存在? - Jason C
@Klemens(另请参见is-ahas-a... 例如,你不能真的说蛇游戏是一个键盘事件,但你可以说它是一个键盘事件处理程序 - 以这种方式思考通常会使设计变得更清晰)。 - Jason C
好的,非常感谢!我能请你看一下我的GitHub上没有继承的代码吗? :P 链接 可以尝试将一些变量和方法移动到其他类或文件中吗? - Klemens
@Klemens 如果你的代码已经能够正常工作,那么最好去http://codereview.stackexchange.com上询问。在那里你会得到更多人的意见。首先看一下那里一些成功的问题,以便了解如何撰写你的帖子。 - Jason C

3

在尝试调用KeyboardEvents类内的DirectionKeyboardEvent::direction时,即使你添加一个模板参数并且该参数正好是子类,编译器也无法提前知道KeyboardEvents<SnakeGame>一定会由SnakeGame类扩展。

我的意思是,有人可能会写出这样的代码:

KeyboardEvents<SnakeGame> keyboardEvents;

keyboardEvents.chooseMethodFromKeyboardArrows(/* some key */);

在这种情况下,`keyboardEvents`与`SnakeGame`并没有太多关联。实际上根本没有创建`SnakeGame`实例!编译器是正确的,调用`direction`的函数`chooseMethodFromKeyboardArrows`错误地假设`KeyboardEvents<SnakeGame>`是`SnakeGame`。
继承的工作方式正好相反:`SnakeGame`确实是一个`KeyboardEvents<SnakeGame>`。另一种方式是错误的。
我可以向您展示如何“使其工作”,但是需要在此发出警告:您正在过度使用继承,并且在`KeyboardEvent`的情况下使用了错误的方式。您真的应该尝试重新排列事物,否则您最终会陷入困境。
解决方案“使其工作”
由于您正在使用CRTP,因此可以告诉编译器,在绝对的所有情况下,`KeyboardEvents<SnakeGame>`都是被`SnakeGame`扩展的。如果确实是这种情况,那么您只需将基类强制转换为子类即可:
if(iterator!=mapOfDirects.end()){
    // Notice the presence of the cast here
    iterator->second->direction(static_cast<SnakeGame&>(*this));
    mainDirection=codeFromKeyboard;
}

稍微更好的解决方案

你也可以使用已有的蛇类实例作为参数。

void chooseMethodFromKeyboardArrows(sf::Keyboard::Key codeFromKeyboard, SakeGame& game){
    auto iterator = mapOfDirects.find(codeFromKeyboard);

    if(iterator!=mapOfDirects.end()){
        iterator->second->direction(game);
        mainDirection=codeFromKeyboard;
    } else {
        mapOfDirects[mainDirection]->direction(game);
    }
}

然而,最好的方法是不让 SnakeGame 扩展 KeyboardEvent,而是将其包含在类中:
struct SnakeGame : Screen {
    KeyboardEvent<SnakeGame> event;

    void callEvents() {
        event.chooseMethodFromKeyboardArrows(/* some key */, *this);
    }
};

这里有一个作业需要你完成:

尝试将KeyboardEvent类变成非模板。我相信你可以想出一种方法,在不使用模板的情况下传递你的类,同时直接访问你的SnakeGame类,而无需进行强制转换或接口。


1
没问题!但是请在你的代码中加入一些爱并适当地格式化它 :P - Guillaume Racicot
1
@Guillaume 我喜欢适当的格式和对代码的关注之间的联系。我从未这样想过,哈哈。 - Jason C

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接