我做了一件阴暗的事

时间:2011-04-08 23:40:31

标签: c++ incomplete-type

由于实际原因(看似)阴暗的东西是否可以接受?

首先,我的代码有一些背景知识。我正在编写2D游戏的图形模块。我的模块包含两个以上的类,但我在这里只提两个: Font GraphicsRenderer

字体 提供了一个接口,通过它可以加载(和释放)文件,而不是更多。在我的 Font 标头中,我不希望泄漏任何实现细节,包括我正在使用的第三方库的数据类型。我阻止第三方lib在标题中可见的方式是通过不完整的类型(我明白这是标准做法):

class Font
{
  private:
    struct FontData;
    boost::shared_ptr<FontData> data_;
};

GraphicsRenderer (读取:单例)设备,用于初始化和完成第三方图形库,还用于呈现图形对象(例如字体图像等)。它是单身的原因是因为,正如我所说,该类自动初始化第三方库;它在创建单例对象时执行此操作,并在单例销毁时退出库。

无论如何,为了使 GR 能够呈现 Font ,它显然必须能够访问其 FontData 对象。一种选择是拥有一个公共getter,但这会暴露 Font 的实现(除了 Font GR 之外没有其他类应该关心关于 FontData )。相反,我认为最好让 GR 成为 Font 的朋友。

注意:到目前为止,我已经完成了一些有些人可能会认为是阴暗的事情(单身人士和朋友),但这些并不是我想问你的事情。尽管如此,如果您认为我的 GR 单身人士和 Font 的朋友的理由是错误的,请批评我并提供更好的解决方案。

阴暗的事情。所以 GR 可以通过友情访问 Font :: data _但是它怎么知道究竟是什么< em> FontData 是(因为它没有在标题中定义,它是一个不完整的类型)?我只会展示包含基本原理的代码和评论......

// =============================================================================
//   graphics/font.cpp
// -----------------------------------------------------------------------------

struct Font::FontData
    : public sf::Font
{
    // Just a synonym of sf::Font
};

// A redefinition of FontData exists in GraphicsRenderer::printText(),
// which will have to be modified as well if this definition is modified.
// (The redefinition is called FontDataSurogate.)
// Why not have FontData defined only once in a separate header:
// If the definition of FontData changes, most likely printText() text will
// have to be altered also regardless. Considering that and also that FontData
// has (and should have) a very simple definition, a separate header was
// considered too much of an overhead and of little practical advantage.


// =============================================================================
//   graphics/graphics_renderer.cpp
// -----------------------------------------------------------------------------

void GraphicsRenderer::printText(const Font& fnt /* ... */)
{
    struct FontDataSurogate
        : public sf::Font {
    };

    FontDataSurogate* suro = (FontDataSurogate*)fnt.data_.get();
    sf::Font& font = (sf::Font)(*suro);

    // ...
}

所以这是我想要做的阴暗的事情。基本上我想要的是审查我的理由,所以请告诉我,如果你认为我做了一些可怕的事情或者如果没有确认我的理由那么我可以有点确定我做得对事情。 :)(这是我最大的项目,我只是在开始时所以我有点感觉黑暗中的东西。)

4 个答案:

答案 0 :(得分:3)

一般来说,如果事情看起来很粗略,我发现它往往值得回去几次,并试图弄清楚为什么这是必要的。在大多数情况下,会出现某种修复方式(可能不是“很好”,但不依赖于任何技巧)。

现在,我在您的示例中看到的第一个问题是这段代码:

struct FontDataSurogate
    : public sf::Font {
};

在不同的文件中出现两次(都不是标题)。这可能会回来,当你改变一个而不是另一个时,你会感到麻烦,并确保两者相同将很可能是一种痛苦。

要解决这个问题,我建议将定义放到FontDataSurogate,并将相应的包含(无论库/标题定义sf::Font)放在单独的标题中。从需要使用FontDataSurogate的两个文件中,包括该定义标头(不是来自任何其他代码文件或标头,只是那两个)。

如果你有一个库的主类声明头,那么在那里放置类的前向声明,并在对象和参数中使用指针(常规指针或共享指针)。

然后,您可以使用friend或添加get方法来检索数据,但是通过将类定义移动到其自己的标头,您已创建该代码的单个副本并具有单个对象/文件那是与其他图书馆的接口。

修改 在我写这篇文章时你对这个问题发表了评论,所以我会在你的评论中添加回复。

  

“太多开销” - 更多文档,还包括一件事,代码的复杂性增加等等。

不是这样。与现在必须保持相同的两个相比,您将拥有一个代码副本。代码以任何一种方式存在,因此需要记录,但您的复杂性和特别是维护得以简化。你确实获得了两个#include语句,但这样的成本是否很高?

  

“实用性很小” - 每次修改FontData时都必须修改printText(),无论它是否在单独的标题中定义。

优点是重复代码更少,使您(以及其他人)更容易维护。在输入数据发生变化时修改功能实际上并不令人惊讶或不寻常。将它移动到另一个标题不会花费你任何东西,但提到的包括。

答案 1 :(得分:2)

friend很好,也很鼓励。有关详细信息,请参阅C ++ FAQ Lite的基本原理:Do friends violate encapsulation?

这一行确实很可怕,因为它调用了undefined behaviorFontDataSurogate* suro = (FontDataSurogate*)fnt.data_.get();

答案 2 :(得分:0)

您转发声明FontData结构的存在,然后继续在两个位置完全声明它:Font和GraphicsRenderer。 EW。现在你必须手动保持这些二进制兼容。

我确信它有效,但你是对的,它有点阴暗。但是每当我们说这样的东西是eeevil 时,我们的意思是避免某种做法,但需要注意的是,它有时会有用。话虽如此,我认为这不是其中之一。

一种技巧是颠倒你的处理。而不是将所有逻辑放在GraphicsRenderer中,而是将其中的一些放在Font中。像这样:

class Font
{
  public:
    void do_something_with_fontdata(GraphicsRenderer& gr);

  private:
    struct FontData;
    boost::shared_ptr<FontData> data_;
};

void GraphicsRenderer::printText(const Font& fnt /* ... */)
{
   fnt.do_something_with_fontdata(*this);
}

这样,Font详细信息保存在Font类中,甚至GraphicsRenderer也不需要知道实现的细节。这也解决了friend问题(虽然我不认为朋友使用起来很糟糕)。

根据代码的布局方式以及代码的运行方式,尝试将其反转可能非常困难。如果是这种情况,只需将FontData的真实声明移至其自己的头文件,并在FontGraphicsRenderer中使用它。

答案 3 :(得分:0)

你花了很多精力问这个问题然后你应该通过复制代码来保存。

您说明了您不想添加文件的三个原因:

  1. 额外包含
  2. 额外文档
  3. 额外的复杂性
  4. 但是我必须通过复制该代码来说2和3 增加。现在,您将记录它在原始位置所做的事情,以及它在代码库中另一个随机位置再次定义的炒猴子。复制代码只会增加项目的复杂性。

    您唯一保存的是包含文件。但文件很便宜。你不应该害怕创造它们。几乎没有成本(或至少应该有)添加新的头文件。

    正确地做到这一点的好处:

    1. 编译器不必使您提供兼容的定义
    2. 有一天,有人会修改FontData类而不修改PrintText(),也许他们应该修改PrintText(),但他们要么还没有完成它,要么不知道他们需要。或者也许是以一种简单的方式对FontData上的其他数据没有意义。无论如何,不​​同的代码片段将在不同的假设下运行,并且会在非常难以追踪的bug中爆炸。