【问题标题】:Is there any reason that the following pimpl-like implementation wouldn't work?以下类似 pimpl 的实现是否有任何原因不起作用?
【发布时间】:2014-01-27 04:17:41
【问题描述】:

Here 我问了一个问题,试图避免使用 window.h 文件中的内容污染我的代码库,以确保我的代码库是跨平台兼容的。有人向我展示了我提出的一般想法是指 pimpl 成语。今天我把这个成语向前或向后迈了一步,我不确定是哪一个。我相当确定我的实现在技术上不会引起问题,但有点难看。我的主要问题是:有人知道实施会导致问题的任何原因吗?还有什么方法可以让它不那么……嗯……丑,更容易维护?

文件 ElementsToHide.h 模拟我不想污染整个项目的全局命名空间的文件的内容,例如 windows.h。

文件 ClassWithPImpl.h 包含一个 PImpl 结构的定义,该结构仅包含一个 char[16],但仅在没有定义标志以指示其先前声明的情况下才定义该结构。 char[16] 只是为了在所有文件中保持结构的大小相同。

ClassWithPImpl.cpp 文件当然定义了类的功能,但与可能包含 ClassWithPImpl.h 的任何其他文件不同,ClassWithPImpl.cpp 定义了它自己的 PImpl 结构版本,它使用来自 ElementsToHide.h 的数据类型。这两个结构体的大小是一样的,也就是说ClassWithPImpl.cpp在编译的时候可以看到PImpl结构体的实际内容。每个其他文件只看到一个私有的 char 数组。这意味着没有其他文件包含 ElementsToHide.h,因此没有其他文件被它的数据类型、函数等污染...

main.cpp 是一个小程序,它打印出类和 PImpl 结构的大小以确保它们相同。商业应用程序可能会使用断言来确保结构保持相同的大小。

以下是应用程序中的所有文件: ElementsToHide.h

#ifndef ELEMENTS_TO_HIDE_H
#define ELEMENTS_TO_HIDE_H

typedef short int16;
typedef long int32;
typedef long long int64;

#endif

ClassWithPImpl.h

#ifndef CLASS_WITH_PIMPL_H
#define CLASS_WITH_PIMPL_H

#ifndef PIMPL_DEFINED
#define PIMPL_DEFINED
struct PImpl
{
    char data[16];
};
#else
struct PImpl;
#endif

class ClassWithPImpl
{
private:
    PImpl pImpl;
public:
    ClassWithPImpl();
    void print();
};

#endif

ClassWithPImpl.cpp

#include <iostream>
using namespace std;

#include "ElementsToHide.h"

#define PIMPL_DEFINED
struct PImpl
{
    int16 shortInt;
    int32 mediumInt;
    int64 longInt;
};

#include "ClassWithPImpl.h"

ClassWithPImpl::ClassWithPImpl()
{
    pImpl.shortInt = 4;
    pImpl.mediumInt = 1;
    pImpl.longInt = 2;
}

void ClassWithPImpl::print()
{
    cout << pImpl.shortInt << endl;
    cout << "Size of class as seen from class: " << sizeof(ClassWithPImpl) << endl;
    cout << "Size of PImpl as seen from class: " << sizeof(PImpl) << endl;
}

main.cpp

#include <iostream>
using namespace std;

#include "ClassWithPImpl.h"

int main()
{
    //int32 shortInt;
    //Program won't compile with this line, because despite the ClassWithPImpl using the int32 type defined in ElementsToHide.h, these types are never
    //actually included into main.cpp.
    ClassWithPImpl().print();
    cout << "Size of Class as seen from Main: " << sizeof(ClassWithPImpl) << endl;
    cout << "Size of PImple as seen from Main: " << sizeof(PImpl) << endl;
    cout << sizeof(short) << ", " << sizeof(long) << ", " << sizeof(long long) << endl;
}

应用程序的输出是:

4
Size of class as seen from class: 16
Size of PImpl as seen from class: 16
Size of Class as seen from Main: 16
Size of PImple as seen from Main: 16
2, 4, 8

如您所见,类文件可以很好地查看和处理内部变量,而应用程序的其余部分则真正忘记了应用程序具有除 char[16] 之外的任何内部变量。默认的复制构造函数/操作符应该只工作文件,因为它只会复制数组内容。不需要析构函数,因为类中没有托管资源。引用指向隐藏数据结构的指针也不会浪费 CPU 时间,从实现文件查看时,它实际上直接包含在类定义中。这是丑陋的。是否有人知道这不起作用或会使情况进一步复杂化的情况?有没有什么方法可以简化它,而不需要在任何时候你想从中获取数据时都必须管理和取消引用的指针?

【问题讨论】:

  • 您可能想阅读 GotW#28 - The Fast Pimpl Idiom at gotw.ca/gotw/028.htm
  • 读起来很有趣。仍然使用 Pimpl 习惯用法,但使用自定义分配器来加速 Pimpl 的分配/释放。老实说,我仍然认为我更喜欢他的尝试#3。无需任何额外的分配/解除分配。隐藏的类变量不需要经过额外的指针取消引用。使用它本身并不需要复制构造函数/运算符。我必须承认,我只是更喜欢#3。

标签: c++ pointers struct cross-platform pimpl-idiom


【解决方案1】:

pointer-to-implementation 习惯用法使用...等等...一个指针!因此,本来是私有的数据动态分配给new。您发布的代码使用了一个不透明的缓冲区,您希望在不会过度浪费的情况下保持足够的速度,这会损害 pImpl 的一些好处,但会节省动态工作时的分配和释放时间。因此,最好不要将其称为 pImpl。

我的主要问题是:有人知道实施会导致问题的任何原因吗?

#ifndef 跨翻译单元更改数据隐藏类的内容会根据 3.2/6 提供未定义行为:

D 的每个定义都应由相同的标记序列组成;

因此,不能保证它会起作用。如果您感到幸运,您可能仍想检查标准是否保证将您的类对象放在适当对齐的边界上——即使“内容”表面上是一个字符数组,或者您的实现没有做到这一点,或者有一个编译器指令来请求它。

您需要小心地构造和销毁放置在缓冲区中的任何成员数据(我建议您对整个 struct 使用放置新的和显式销毁 - 如果方便,给它构造函数 - 而不是分配给单个成员),并且应该保持静态断言以确保缓冲区足够大。

默认的复制构造函数/操作符应该只对文件起作用,因为它只会复制数组内容。不需要析构函数,因为类中没有托管资源。

也许你现在确实如此,但你真的认为其他程序员在他们只想添加std::string、共享指针或其他东西时肯定会仔细检查你所做的事情吗?

还有什么方法可以让它不那么……嗯……丑,更容易维护?

没有什么值得的事情浮现在脑海中。

【讨论】:

  • 啊哈!我假设 Pimpl 是 Private Implementation 的缩写。如果它代表 Pointer-To-Implementation,那么从技术上讲,它显然不是 PImpl 实现。更新了主题标题以更合适。其余的问题是为什么我希望找到一些更好的方法来做到这一点,我对自己理解实现工作原理的能力充满信心......但我对每个程序员甚至大多数程序员都没有信心程序员将能够理解正在做什么、如何做以及为什么做。
  • @Darinth:我认为 85%+ 的专业 C++ 程序员会理解结构附近的三四行 cmets。我已经多次看到这种“重新发明”,所以很多人都明白其中的动机。不过我会放弃#ifndef,只使用一个简单的私有成员函数,重新解释转换字符数组以返回对实际数据结构的引用。
  • 我什至没有想过使用 reinterpret_cast。只需使用这种技术重新编写类,它就有很多优点,我找不到任何缺点。编译成相同的汇编代码。每个翻译单元看到的类完全相同,我可以轻松地去掉所有的#defines、#ifndefs 等......我只是使用 reinterpret_cast 来通知编译器包含的数据的实际结构是什么。构造函数中的单个 static_assert 保证类中留出的数据空间始终与实际数据的大小匹配。
  • 嗯,“每个翻译单元看到的类完全相同”是错误的 - 一个看到 chars 另一个任何数据成员,“保证类中留出的数据空间始终匹配” - 删除一个pImpl 成语提供的主要解耦好处。无论如何 - 选择你的毒药。如果你想“清理”一些考虑一个可重用的基类,它包含char[] 和一些访问函数:你可以强制安全的成员构造,提供包含类可以调用的赋值和销毁函数模板, static_assert 大小等......
猜你喜欢
  • 2019-01-24
  • 1970-01-01
  • 1970-01-01
  • 2015-07-28
  • 1970-01-01
  • 1970-01-01
  • 2014-01-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多