通过减少填充头的数量来隐藏实现细节

hiding implementation details by reducing number of populated headers

本文关键字:隐藏 实现 细节 填充      更新时间:2023-10-16

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

我也有一个内部引擎,不应该从外部调用。

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

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都有成员变量

您正在使用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; }

main.cpp

#include "interface.hpp"
int main()
{
    auto interface = create();
    interface->some_method();
    return 0;
}

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

关于PIMPL和接口的更多讨论在这里和这里

编辑:

基于STL容器和Howard Hinnant的堆栈分配器,第三种避免自由存储中的变量的方法是:

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);
}

我无法理解,为了解决隐藏实现细节的问题,我必须改变我的设计并在自由存储上分配引擎。

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

我知道它是关于知道引擎类的大小

。如果interfaceengine类型的成员,则必须知道该成员的大小,否则无法知道interface本身的大小。不完整字型的大小是未知的。定义成员类型可以解决这个问题,但与隐藏实现的愿望相冲突。

有更好的解决方案吗?

没有更好的解决方案。带有免费存储的PIMPL——这是您目前使用的模式——是最好的。

从技术上讲,PIMPL不要求您使用免费存储。您可以将对象存储在您喜欢的任何地方。您可以使用静态存储。但这会限制实例的数量。你甚至可以分配一个内存缓冲区作为interface的成员,但是你需要硬编码这个缓冲区的大小,它必须匹配engine的大小(和对齐方式)。

我认为这两种理论建议都是拼凑的,尤其是后者。如果您的分析表明该特定额外分配的开销很大,并且不能选择放弃PIMPL,那么静态存储可能是值得的。

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

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

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

请注意,您的实现的某些方面将被公开,因为它将影响库的ABI。没有一种解决方案可以既给你 ABI绝缘,又避免在堆上分配对象。

很可能你最好使用pimpl,正如你所描述的。