PIMPL习语的目的是隐藏实现,包括方法、结构,甚至结构的大小。一个缺点是它使用堆。
但是,如果我不想隐藏任何东西的大小要求怎么办?我只是想隐藏方法、结构格式和变量名。一种方法是分配一个完美大小的字节数组,让实现不断地将其转换为任何结构并使用它。但是手动找到要为对象分配的字节大小?一直都在做角色转换吗?显然不实际。
是否有一种对PIMPL或不透明指针有利的习惯用法或通用方法来处理这种情况?
另一种方法是重新思考对象真正表示的本质。在传统的OOP中,习惯上认为所有对象都是具有自己的数据和方法的自包含实体。其中一些方法对类来说是私有的,因为它们只是类自己的内务管理所必需的,所以这些是你通常移走Pimpl类的"impl"的那种东西。
在最近的一个项目中,我一直喜欢领域驱动设计方法,其中一个可取之处是将数据与处理数据的逻辑分开。然后,数据类就变成了结构体,而以前隐藏在Pimpl中的复杂逻辑现在可以放在一个没有自己状态的Service对象中。
考虑一个游戏循环的例子:
class EnemySoldier : public GameObject
{
public:
// just implement the basic GameObject interface
void updateState();
void draw(Surface&);
private:
std::unique_ptr<EnemySoldierImp> m_Pimpl;
};
class EnemySolderImpl
{
public:
// 100 methods of complex AI logic
// that you don't want exposed to clients
private:
StateData m_StateData;
};
void runGame()
{
for (auto gameObject : allGameObjects) {
gameObject->updateState();
}
}
这可以重新构造,以便GameObjects
不管理它们的数据和它们的程序逻辑,我们将这两件事分开:
class EnemySoldierData
{
public:
// some getters may be allowed, all other data only
// modifiable by the Service class. No program logic in this class
private:
friend class EnemySoldierAIService;
StateData m_StateData;
};
class EnemySoldierAIService
{
public:
EnemySoldierAIService() {}
void updateState(Game& game) {
for (auto& enemySoldierData : game.getAllEnemySoldierData()) {
updateStateForSoldier(game, enemySoldierData);
}
}
// 100 methods of AI logic are now here
// no state variables
};
我们现在不需要任何pimps或任何关于内存分配的技巧。我们还可以使用游戏编程技术,通过将全局状态存储在几个平面向量中而不是需要指向基类的指针数组来获得更好的缓存性能和减少内存碎片,例如:
class Game
{
public:
std::vector<EnemySoldierData> m_SoldierData;
std::vector<MissileData> m_MissileData;
...
}
我发现这种通用的方法确实简化了很多程序代码:
- 对痘痘的需求减少
- 程序逻辑都在一个地方
- 通过在运行时选择Service类的V1和V2版本来保持向后兼容性或放弃替代实现要容易得多
- 少堆分配
您试图隐藏的信息与编译器计算大小所需的信息完全相同。也就是说,不,没有在不知道非静态成员的数量和数据类型的情况下查找大小的习惯用法,因为这甚至是不可能的。
另一方面,您可以很好地隐藏helper函数的存在。只需声明一个嵌套类型(这使嵌套成员可以访问外部类的私有成员),并仅在私有实现文件中定义该类型,将helper逻辑放在嵌套类型的静态成员函数中。您必须将指向要操作的对象实例的指针作为参数传递,但随后您可以访问所有成员。的例子:
class FlatAPI
{
void helperNeedsPublicAccess();
void helperNeedsFullAccess();
T data;
public:
void publicFunction();
};
是
class PublicAPI
{
struct helpers;
T data;
public:
void publicFunction();
};
和实现代码
#include <public.h>
static void helperNeedsPublicAccess(PublicAPI* pThis) { pThis->publicFunction(); }
struct PublicAPI::helpers
{
static void helperNeedsFullAccess(PublicAPI* pThis) { std::cout << pThis->data; }
};
void PublicAPI::publicFunction()
{
helpers::helperNeedsFullAccess(this);
}
所以这里有一个可能的替代方案,它没有常量强制转换的缺点,但改善了内存布局,使其类似于您根本没有使用PIMPL。
我要假设你的应用程序并没有真正使用一个粉刺,但实际上你使用了许多类的粉刺,所以就像,第一个粉刺的粉刺包含了许多子类的粉刺,而那些粉刺包含了许多第三层类的粉刺,等等。
(我设想的对象类型是,所有的管理器,调度程序,应用程序中的各种引擎,很可能不是所有的实际数据记录,这些可能在一个管理器拥有的标准容器中。但是,在应用程序的过程中,您通常只拥有固定数量的所有对象
第一个想法是,类似于std::make_shared
的工作原理,我想在"助手"对象旁边分配主对象,这样我就可以在不破坏封装的情况下获得"快速"内存布局。我这样做的方法是为两者分配一个足够大的连续内存块,并使用placement new,这样pimpl就在impl旁边。
本身并没有什么改进,因为pimpl只是指针的大小,而拥有pimpl的人现在需要一个指向pimpl的指针,因为它现在是堆分配的。
然而,现在我们基本上尝试一次对所有图层做这个。
实际需要什么才能使它工作:
- 每个pimpl类需要公开一个静态成员函数,该函数在运行时可用,该函数以字节为单位指示其大小。如果对应的表达式很简单,这可能就是
return sizeof(my_impl)
。如果对应的impl包含其他的粉刺,则这是return sizeof(my_impl) + child_pimpl1::size() + child_pimpl2::size() + ...
。 - 每个pimpl类都需要一个自定义的
operator new
或类似的工厂函数,该函数将分配给给定大小的内存块- 粉刺和它的impl(减去你递归处理的粉刺子)
- 每个丘疹患儿依次,使用其相应的
operator new
或类似功能。
现在,在你的应用程序开始时,你做了一个巨大的堆分配,它包含"根"管理器对象或任何相应的对象。(如果没有,那么你可以为这个目的引入一个。)你在那里使用它的工厂函数,连续分配所有这些对象。
我认为这基本上提供了相同的好处,如果你让所有的痘痘持有char[]
的大小正好合适,并不断铸造的东西。只有当你只需要固定数量的这些元素,或者不需要太多的时候,它才会有效。如果您需要经常拆除和重建这些对象,这是可以的,因为您只需手动调用析构函数并使用placement new进行重建。但是,在应用程序结束之前,您将无法真正返回任何内存,因此这涉及到一些权衡。
PIMPL习语的目的是隐藏实现,包括方法,结构,甚至结构的大小。
参见http://herbsutter.com/gotw/_100/
一个缺点是它使用堆。
我认为使用堆是有好处的。Stack更有价值,而且更有限(8mb vs 3gb在我的硬件上)。
但是,如果我不想隐藏的大小要求任何东西。
我的想象力失败了很多次。我试着假设你知道你想要什么,以及为什么你想要它。
恕我直言,没有隐藏尺寸信息并不重要。
我只是想隐藏方法,以及结构和格式
我认为你仍然需要揭露医生和医生(或命名替代,即createFoo/removeFoo)
一种方法是分配一个完美大小的字节数组,
这很容易做到。
让实现不断地将其强制转换为任何结构和使用。
IMHO不需要转换(我从来没有需要它-见下面的MCVE)
但是,即使你投了一些我猜不到的原因,记住,强制类型转换(没有转换)不会产生任何代码,因此没有性能问题。
但是手动找到要分配给对象的字节大小?
实际上,这只是开发过程中的一个小挑战(当大小可能改变时)。在我早期的职业生涯中,我有最初猜测了十几次努力,通常使用比必要的数据大小估计稍大一些,以适应增长。
然后添加一个运行时断言(您可能更喜欢使用"if子句")当大小大于目标时生成通知。根据我的经验,数据大小总是很快稳定下来。
让大小信息精确是很简单的(如果你想的话)。
总是强制类型转换吗?显然不现实。
我不明白为什么你认为这涉及到类型转换。我不使用在我创造的痘痘中(也不是在下面的MCVE中)。
我不明白为什么你(以及至少另外一个人)认为类型转换不是实用。非转换强制转换不需要任何成本(在运行时),并且是完全由编译器处理。也许有一天我会问一些相关的问题。甚至我的编辑器也可以自动转换前缀。
我不明白为什么至少有一个评论认为存在空白要转换的指针。
我不知道有这样的成语。我发现了几个例子符合我对处理粉刺的期望,这样他们就将军?可能不会。但是,请注意,从我多年的嵌入式系统工作经验来看,我考虑下面列出的总结的想法/需求相对简单挑战。是否有处理这种情况的习惯用法或一般方法对PIMPL或不透明指针有利。
欢迎反馈。
总结要求:
取消大小信息隐藏。
隐藏方法(例外:ctor和dtor或命名的ctor/dtor替代品)
分配字节数组(implBuff)作为疙瘩属性的位置。
- 手动提供痘痘大小信息
提供输出来计算当前的impl大小(以简化开发)
当手动impbuff的大小太小而不能容纳实际impl
当手动大小的implBuff太浪费空间
说明为什么不需要强制转换(嗯,否定证明很难。不如我直接显示代码,不需要强制转换)
- 记录病态依赖,如果容易,显示实用的解决方案
注意:
这些选择有时并非没有"病态依赖",我发现了两个,我认为很容易处理或忽略。见下文.
下面的MCVE构建并运行在我的Ubuntu 15.04, g++ ver 4.9.2-10ubuntu13
示例输出如下代码:
#include <iostream>
#include <sstream>
#include <vector>
#include <cassert>
// ///////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////////
// file Foo.hh
class Foo // a pimple example
{
public:
Foo();
~Foo();
// alternative for above two methods: use named ctor/dtor
// diagnostics only
std::string show();
// OTHER METHODS not desired
private:
// pathological dependency 1 - manual guess vs actual size
enum SizeGuessEnum { SizeGuess = 24048 };
char implBuff [SizeGuess]; // space inside Foo object to hold FooImpl
// NOTE - this is _not_ an allocation - it is _not_ new'd, so do not delete
// optional: declare the name of the class/struct to hold Foo attributes
// this is only a class declaration, with no implementation info
// and gives nothing away with its name
class FooImpl;
// USE RAW pointer only, DO NOT USE any form of unique_ptr
// because pi does _not_ point to a heap allocated buffer
FooImpl* pi; // pointer-to-implementation
};
// ///////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////////
// top of file Foo.cc
typedef std::vector<std::string> StringVec;
// the impl defined first
class Foo::FooImpl
{
private:
friend class Foo; // allow Foo full access
FooImpl() : m_indx(++M_indx)
{
std::cout << "n Foo::FooImpl() sizeof() = "
<< sizeof(*this); // proof this is accessed
}
~FooImpl() { m_indx = 0; }
uint64_t m_indx; // unique id for this instance
StringVec m_stringVec[1000]; // room for 1000 strings
static uint64_t M_indx;
};
uint64_t Foo::FooImpl::M_indx = 0; // allocation of static
// Foo ctor
Foo::Foo(void) : pi (nullptr)
{
// pathological dependency 1 - manual guess vs actual size
{
// perform a one-time run-time VALIDATE of SizeGuess
// get the compiler's actual size
const size_t ActualSize = sizeof(FooImpl);
// SizeGuess must accomodate entire FooImpl
assert(SizeGuess >= ActualSize);
// tolerate some extra buffer - production code might combine above with below to make exact
// SizeGuess can be a little bit too big, but not more than 10 bytes too big
assert(SizeGuess <= (ActualSize+10));
}
// when get here, the implBuff has enough space to hold a complete Foo::FooImpl
// some might say that the following 'for loop' would cause undefined behavior
// by treating the code differently than subsequent usage
// I think it does not matter, so I will skip
{
// 0 out the implBuff
// for (int i=0; i<SizeGuess; ++i) implBuff[i] = 0;
}
// pathological dependency 2 - use of placement new
// --> DOES NOT allocate heap space (so do not deallocate in dtor)
pi = new (implBuff) FooImpl();
// NOTE: placement new does not allocate, it only runs the ctor at the address
// confirmed by cout of m_indx
}
Foo::~Foo(void)
{
// pathological dependency 2 - placement new DOES NOT allocate heap space
// DO NOT delete what pi points to
// YOU MAY perform here the actions you think are needed of the FooImpl dtor
// or
// YOU MAY write a FooImpl.dtor and directly invoke it (i.e. pi->~FooImpl() )
//
// BUT -- DO NOT delete pi, because FOO did not allocate *pi
}
std::string Foo::show() // for diagnostics only
{
// because foo is friend class, foo methods have direct access to impl
std::stringstream ss;
ss << "nsizeof(FooImpl): " << sizeof(FooImpl)
<< "n SizeGuess: " << SizeGuess
<< "n this: " << (void*) this
<< "n &implBuff: " << &implBuff
<< "n pi->m_indx: " << pi->m_indx;
return (ss.str());
}
int t238(void) // called by main
{
{
Foo foo;
std::cout << "n foo on stack: " << sizeof(foo) << " bytes";
std::cout << foo.show() << std::endl;
}
{
Foo* foo = new Foo;
std::cout << "nfoo ptr to Heap: " << sizeof(foo) << " bytes";
std::cout << "n foo in Heap: " << sizeof(*foo) << " bytes";
std::cout << foo->show() << std::endl;
delete foo;
}
return (0);
}
示例输出:
// output
// Foo::FooImpl() sizeof() = 24008
// foo on stack: 24056 bytes
// sizeof(FooImpl): 24008
// SizeGuess: 24048
// this: 0x7fff269e37d0
// &implBuff: 0x7fff269e37d0
// pi->m_indx: 1
//
// Foo::FooImpl() sizeof() = 24008
// foo ptr to Heap: 8 bytes
// foo in Heap: 24056 bytes
// sizeof(FooImpl): 24008
// SizeGuess: 24048
// this: 0x1deffe0
// &implBuff: 0x1deffe0
// pi->m_indx: 2