【问题标题】:hiding implementation details by reducing number of populated headers通过减少填充标头的数量来隐藏实现细节
【发布时间】:2016-08-18 08:48:23
【问题描述】:

我正在开发一个库。我有可以从外部调用的接口类。

我还有一个不应从外部调用的内部引擎。

当我在这里和那里阅读时,我应该隐藏内部引擎类,甚至不填充它的标题。由于我有以下结构:

interface.hpp:

#include "engine.hpp" 
class interface{
private:
    enigne eng;
};

interface.cpp:

#include "engine.hpp"
//code that uses member variables and functions from eninge 

engine.hpp:

class engine{};

要解决填充“engine.hpp”的问题,我应该将代码更改为:

interface.hpp:

class engine;
class interface{
private:
    some_smart_pointer<enigne> eng_ptr;
};

interface.cpp:

#include "engine.hpp"
//code that uses member variables and functions from eninge 

enigne.hpp:

class engine{};

这解决了问题。但是,从现在开始,engine 是动态分配的。它的所有成员变量都在免费存储中。

我无法理解我必须更改我的设计并在免费商店上分配引擎来解决隐藏实现细节的问题。有没有更好的解决方案?

附:我不是在问为什么这个解决方案有效。我知道如果我将它留在堆栈上,那么知道引擎类的大小是强制性的。我的问题是关于要求可能解决问题的不同设计。

编辑:

interfaceengine 都有成员变量。

【问题讨论】:

  • 提供的任何解决方案只会使您的实施复杂化。如果您担心使用标准分配器的成本(元数据浪费和/或速度),you can always write your own allocator
  • 如果Interface没有成员,只有虚函数,你可以完全隐藏Engine
  • 总有一个 hacky 解决方案:使用单个私有 char data[&lt;size of engine&gt;]static_cast&lt;engine&gt;(data)。但这是一场维护噩梦,而且真的 hacky。相反,编写自己的分配器可能几乎一样有效,但更清洁。当然,在进行过早优化之前,首先测量 PIMPL 是否真的是性能瓶颈。
  • 有一个权衡。如果您更重视隐藏实现而不是简化实现,那么这就是如何做到这一点。如果你更看重一个简单的实现而不是隐藏实现,那么不要这样做。任何告诉您必须始终隐藏实现的人都是错误的。
  • @MartinNyolt 字符缓冲区可能未对齐;您必须确保它与引擎的数据成员正确对齐。

标签: c++ c++11


【解决方案1】:

您使用的是 PIMPL 成语。另一种隐藏实现的方法是使用接口,即抽象基类和工厂函数:

interface.hpp:

class interface{
public:
    virtual ~interface(){}
    virtual void some_method() = 0;
};

// the factory function
static some_smart_pointer<interface> create();

interface.cpp:

#include "interface.hpp"
#include "engine.hpp"

class concrete : public interface{
public:
    virtual void some_method() override { /* do something with engine */ }
private:
    engine eng;
};

some_smart_pointer<interface> create(){ return new concrete; }

ma​​in.cpp

#include "interface.hpp"

int main()
{
    auto interface = create();
    interface->some_method();

    return 0;
}

这里的缺点是你必须动态分配interface而不是engine

更多关于 PIMPL 和接口 herehere 的讨论

编辑:

基于 STL 容器和 Howard Hinnant 的 stack allocator,第三种避免免费存储中变量的方法可以是:

interface.hpp:

class interface{
public:
    interface();
    ~interface();

    // I disable copy here, but you can implement them
    interface(const interface&) = delete;
    interface& operator=(interface&) = delete;

private:
    engine* eng;
};

interface.cpp:

#include "interface.hpp"
#include "engine.hpp"
#include "short_alloc.h"

#include <map>

namespace
{
    std::map<
        interface*,
        engine,
        std::default_order<interface*>,
        // use a stack allocator of 200 bytes
        short_alloc<std::pair<interface*, engine>, 200>
    > engines;
}

interface::interface():
    eng(&engines[this])
        // operator[] implicitly creates an instance and returns a reference to it
        // the pointer gets the address
{
}

interface::~interface()
{
    // destroy the instance
    engines.erase(this);
}

【讨论】:

  • 谢谢..有些东西我听不懂。在这种情况下,引擎不是也分配在免费存储上吗?
  • @HumamHelfawi 我用没有动态分配的实验版本编辑了我的答案。我真的不知道该怎么想,告诉我你的意见
  • 嗯,使用std::map如何避免免费存储?
  • 哦,天哪...我忘记了容器分配。我的错。我编辑了代码以使用stack allocation
【解决方案2】:

我无法理解我必须改变我的设计并在免费商店上分配引擎来解决隐藏实现细节的问题。

你自己好像已经解释过了:

我知道这是关于了解引擎类的大小

确实如此。如果interface 有一个engine 类型的成员,那么它必然需要知道该成员的大小——否则interface 本身的大小就无法知道。无法知道不完整类型的大小。定义成员类型可以解决这个问题,但与您隐藏实现的愿望相冲突。

有没有更好的解决方案?

没有更好的解决方案。带有免费商店的 PIMPL - 这是您当前使用的模式 - 尽其所能。

从技术上讲,PIMPL 并不要求您使用免费商店。您可以将对象存储在您喜欢的任何位置。您可以使用静态存储。但这会限制您可以拥有的实例数量。您甚至可以将内存缓冲区分配为interface 的成员,但您需要对此类缓冲区的大小进行硬编码,并且它必须与engine 的大小(和对齐方式)匹配。

我会将这两个理论建议都归类为杂牌——尤其是后者。如果您的分析表明该特定额外分配的开销很大并且不能选择放弃 PIMPL,则静态存储可能是值得的。

【讨论】:

    【解决方案3】:

    这是这个问题的标准解决方案:它被命名为 PIMPL(指向实现的指针)习语。顾名思义,它总是包含一个指向实现类的指针(或智能指针),因为 C++ 根本不允许这样做:

    class engine;
    class interface{
    private:
        enigne eng;
    };
    

    【讨论】:

    • 这没有回答问题,完全忽略了OP的后记。
    • @StoryTeller 不,它没有。 OP 要求标准设计,pimpl 确实是正确答案。
    • @StoryTeller:如果有更好的解决方案,PIMPL 可能不再是标准。这是一个否定的答案,但事实就是如此。
    • @BЈовић,OP 没有要求 standard 设计,他们指定他们知道标准设计。他们要求不同的设计。
    • @StoryTeller 不,他正在寻找更好的解决方案。粉刺也是答案
    【解决方案4】:

    隐藏实现并且不强制在堆上分配对象的典型解决方案是将它们放在detail命名空间中,并将这些类型的标头放在详细子目录中。

    请注意,您的实现的某些方面将被公开,因为它会影响您库的 ABI。没有任何解决方案可以为您提供 ABI 绝缘并避免在堆上分配对象。

    正如您所描述的,您最好使用 pimpl。

    【讨论】:

      猜你喜欢
      • 2016-09-09
      • 1970-01-01
      • 2013-08-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多