C++ I';我做了一件不光彩的事

C++ I';我做了一件不光彩的事,c++,incomplete-type,C++,Incomplete Type,出于实际原因,(表面上)阴暗的事情是可以接受的吗 首先,介绍一下我的代码背景。我正在编写2D游戏的图形模块。我的模块包含两个以上的类,但在这里我只提到两个:Font和GraphicsRenderer Font提供了一个加载(和发布)文件的界面,仅此而已。在我的字体标题中,我不想泄露任何实现细节,这包括我正在使用的第三方库的数据类型。我防止第三方库在标题中可见的方法是通过不完整的类型(我知道这是标准做法): 这就是我想做的不光彩的事。基本上,我想回顾一下我的基本原理,所以请告诉我你是否认为我做了可

出于实际原因,(表面上)阴暗的事情是可以接受的吗

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

Font提供了一个加载(和发布)文件的界面,仅此而已。在我的字体标题中,我不想泄露任何实现细节,这包括我正在使用的第三方库的数据类型。我防止第三方库在标题中可见的方法是通过不完整的类型(我知道这是标准做法):


这就是我想做的不光彩的事。基本上,我想回顾一下我的基本原理,所以请告诉我你是否认为我做了可怕的事情,或者如果你不确认我的基本原理,那么我可以更确定我做的是正确的事情。:)(这是我迄今为止最大的项目,我才刚刚开始,所以我在黑暗的atm机中感觉到了一些东西。)

朋友
很好,受到了鼓励。参见C++ FAQ Lite的更多信息的原理:


这一行调用时确实令人毛骨悚然:
FontDataSurogate*suro=(FontDataSurogate*)fnt.data_u2;get()

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

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

struct FontDataSurogate
    : public sf::Font {
};
在不同的文件中发生两次(都不是头)。当你将来改变其中一个而不是另一个时,这可能会回来并成为一个麻烦,确保两者完全相同很可能是一件痛苦的事

为了解决这个问题,我建议将定义放在
FontDataSurogate
中,并将适当的include(无论库/头定义了什么
sf::Font
)放在一个单独的头中。从需要使用
FontDataSurogate
的两个文件中,包括该定义头(不来自任何其他代码文件或头,仅这两个)

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

然后,您可以使用
friend
或添加一个get方法来检索数据,但是通过将类定义移动到它自己的头,您已经创建了该代码的一个副本,并且有一个与另一个库交互的对象/文件

编辑: 在我写这篇文章的时候,你对这个问题发表了评论,所以我会补充一个对你评论的回复

“太多的开销”-需要记录的内容更多,需要包含的内容更多,代码的复杂性增加,等等

不是这样。您将拥有代码的一个副本,而现在必须保持相同的两个副本。代码以任何一种方式存在,因此需要文档化,但您的复杂性,尤其是维护,都得到了简化。您确实获得了两条
#include
语句,但这是如此高的成本吗

“几乎没有实际优势”-每次修改FontData时都必须修改printText(),无论它是否在单独的标题中定义


这样做的好处是减少了重复代码,使您(和其他人)更容易维护。当输入数据发生变化时修改函数并不奇怪。将它移动到另一个头不需要花费任何费用,只需支付所提到的费用。

您可以向前声明
FontData
结构的存在,然后在两个位置完全声明它:Font和GraphicsRenderer。电子战。现在您必须手动保持这些完全二进制兼容

我相信它是有效的,但你是对的,它有点阴暗。但是,每当我们说某某是邪恶的时候,我们的意思是避免某种做法,但要注意的是,有时它可能是有用的。尽管如此,我不认为现在是那种时候

一种技巧是反转操纵。与其将所有逻辑放在GraphicsRender中,不如将其中一些放在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);
}
类字体
{
公众:
void do_something_与_fontdata(GraphicsRenderer&gr);
私人:
结构数据;
boost::共享的ptr数据;
};
void GraphicsRenderer::printText(const Font&fnt/*…*/)
{
fnt.用fontdata做些什么(*这个);
}
这样,字体细节就保存在字体类中,甚至GraphicsRender也不需要知道实现的细节。这也解决了
friend
的问题(尽管我不认为friend使用起来很糟糕)


根据你的代码是如何布局的,以及它在做什么,尝试像这样反转它可能是相当困难的。如果是这样的话,只需将
FontData
的真正声明移动到它自己的头文件中,并在
Font
GraphicsRenderer
中使用它。您在问这个问题上花费了很多精力,而复制该代码应该可以节省时间

您陈述了三个不想添加文件的原因:

  • 额外包括
  • 额外文件
  • 额外复杂性
  • 但是我不得不说,2和3是通过复制代码来增加的。现在,您将记录它在原始位置所做的事情,以及它在代码库的另一个随机位置所做的定义。重复代码只会增加项目的复杂性

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

    正确执行此操作的好处:

  • 编译器不必使您给出的定义兼容
  • 总有一天,
    struct FontDataSurogate
        : public sf::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);
    }