C++是否可以避免使用pimpl技巧?

19

据我所知,pimpl模式的存在仅仅是因为C++强制要求你将所有私有类成员放在头文件中。如果头文件只包含公共接口,理论上,类实现的任何更改都不会导致整个程序需要重新编译。

我想知道的是,为什么C++没有设计允许这样的便利。为什么它要求类的私有部分完全显示在头文件中(无意冒犯)?


@Frederick,你不是个“傻瓜”,这是一个很好的问题! - jwfearn
6个回答

11

这与对象的大小有关。h文件被用来确定对象的大小等信息,如果其中没有给出私有成员变量,那么您就不知道需要创建多大的对象。

不过,您可以通过以下方式模拟您所需的行为:

class MyClass
{
public:
   // public stuff

private:
#include "MyClassPrivate.h"
};

这并不是强制性的行为,但它将私有内容从.h文件中分离出来。 缺点是需要维护另一个文件。 此外,在Visual Studio中,智能感知(IntelliSense)对私有成员无效 - 这可能是优点也可能是缺点。


当然!我是多么傻才会问那个问题。不管怎样,谢谢大家。 - Frederick The Fool
6
为什么这个答案被接受了? "MyClassPrivate.h" 和原始头文件一样容易阅读。它仍然需要重新编译。对象的大小是一个小问题。真正的难点是效率和与某些C习惯用法的向后兼容性。 - jfs
11
看到那个 include 语句,我的心灵感到痛苦。 - KJAWolf
1
@J.F. Sebastian 我同意。为什么这是被接受的答案,而且为什么评分如此之高,它完全没有抓住重点。 - eodabash
我不喜欢 include 语句。它是预处理器滥用。 - Matt Melton
显示剩余3条评论

10

我认为这里存在一些混淆。问题不在于头文件,头文件并没有起到什么作用(它们只是在多个源代码文件中包含公共部分的方式)。

问题,至少有一个问题,是在C ++中类声明必须定义所有实例所需的内容,包括公共和私有。(Java也是如此,但外部编译类引用的工作方式使得使用共享头文件之类的东西是不必要的。)

在常见的面向对象技术(不仅限于C ++)中,即使您仅使用公共部分,某人也需要知道所使用的具体类以及如何使用其构造函数来提供实现。第3个中的设备将其隐藏起来。第1个中的做法分离了关注点,无论您是否执行第3个步骤。

  1. 使用仅定义公共部分(主要是方法)的抽象类,并让实现类从该抽象类继承。因此,按照通常的头文件约定,共享的是抽象.hpp。还有一个声明继承类的implementation.hpp,仅传递给实现方法的模块。implementation.hpp文件将#include "abstract.hpp"用于其进行类声明的使用,以便对抽象化接口的声明有单一的维护点。

  2. 现在,如果要强制隐藏实现类声明,则需要某种方式来请求构造具体实例,而不需要拥有特定的完整类声明:无法使用new,也无法使用本地实例。引入帮助函数(包括在其他类上提供对类实例的引用的方法)是替代方法。

  • 在用作抽象类/接口共享定义的头文件中,除了包含函数签名外,还应包含外部辅助函数的函数签名。这些函数应在特定类实现的模块中实现(因此它们可以看到完整的类声明并调用构造函数)。辅助函数的签名可能与构造函数非常相似,但它返回一个实例引用作为结果(该构造函数代理可以返回NULL指针,甚至可以抛出异常,如果您喜欢这种方式的话)。辅助函数构造特定的实现实例,并将其转换为抽象类实例的引用。

  • 任务已完成。

    啊,当只有实现发生变化时,重新编译和重新链接应按您想要的方式工作,避免调用模块的重新编译(因为调用模块不再对实现进行任何存储分配)。


    1
    这似乎不是一个好主意。这是hacky的,并且感觉你正在积极地反对语言而不是使用它!突然间,C语言中的不透明指针看起来像一个干净简单的解决方案... - dietr

    8

    你们都忽略了问题的要点:

    为什么开发人员必须手动输入PIMPL代码?

    对我来说,我能想到最好的答案就是我们没有一个很好的方式来表达C ++代码,使您可以对其进行操作。例如,编译时(或预处理器等)反射或代码DOM。

    C++非常需要其中一个或两者对开发人员可用以进行元编程。

    那么,您可以在公共MyClass.h中编写以下内容:

    #pragma pimpl(MyClass_private.hpp)
    

    然后编写你自己的非常简单的包装器生成器。

    最佳答案!对于每个pimpl类,我不得不一遍又一遍地输入相同的代码,尽管它实际上非常简单。不幸的是,很难编写一些模板或基类,只需执行一次即可完成此操作。也许可以查看这个(https://github.com/oliora/samples)。我更喜欢一个通用解决方案,可以隐藏STL。 - Fabian

    6
    有人会给出更详细的答案,但简短回答有两个方面:编译器需要知道结构体的所有成员以确定存储空间需求,并且编译器需要知道这些成员的顺序以确定偏移量。语言已经相当复杂;我认为在代码中分割结构化数据的定义机制可能会带来一些灾难。
    通常,我总是看到 策略类 用于以 Pimpl 方式定义实现行为。我认为使用策略模式有一些额外的好处 -- 更容易交换实现,可以轻松地将多个部分实现组合成单个单位,从而允许您将实现代码分解为功能性、可重用的单元等。

    3

    可能是因为在将类的实例按值传递、聚合到其他类中等情况下需要知道其大小。

    如果C++不支持值语义,那就没事了,但它确实支持。


    C++并不是支持值语义,而是通过按值解析来提供这些语义(这也是最有效和最简单的方法)。 - Arafangion
    @Ara:你能详细解释一下“按值解析”这个术语吗?我从未听说过。 - fredoverflow
    @Fred:糟糕,你发现了我的一个笔误 - 实际上,我是指“传递”。 :) - Arafangion
    @sbi:我说的不是Luc的答案,而是Araf的评论 - fredoverflow

    2

    是的,但是...

    你需要阅读Stroustrup的《C++设计与演化》一书。它可能会抑制对C++的使用。


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