注意:我已经重写了这个问题,以指明我的意图更清晰,并缩短它。
我正在设计一个有一些要求的库的一部分:
为了达到这个目的,我使用了pimpl习语。
我正在创建的是一种实例化条目树的方法,用户可以在实例化树之后为每个实体添加其他行为。该库稍后由库的其他部分使用以执行某些操作。树中的条目不必在内存中复制或移动,分配后,即使树中的父级更改,其内存地址仍保持固定。
由于其他部分需要访问实现,因此需要某种方式来访问它,同时最好将其限制为客户端代码。
我在原始问题中描述了多种方法,但现在我将介绍我实施的方法,我认为这可能是实现这一目标的最佳方法之一。
// Public header
#pragma once
class EntryImpl;
class Entry final
{
private:
// 3. Friendship with the implementation class
friend class EntryImpl;
EntryImpl* const m_Impl;
public:
// 1. Constructor takes owning pointer to EntryImpl
Entry(EntryImpl* impl) : m_Impl(impl) { }
// 2. Public destructor
~Entry() { delete m_Impl; }
// Public APIs here...
};
// Private header
#pragma once
class EntryImpl final
{
public:
EntryImpl() { }
~EntryImpl() { }
// 4. Provides the library's internals access to the implementation.
static EntryImpl& Get(Entry& entry) { return *entry.m_Impl; }
// As an example function
void DoSomething() { }
// Other stuff the implementation does here...
};
// Public header
#pragma once
class Entry;
class TreeImpl;
class Tree final
{
private:
TreeImpl* const m_Impl;
public:
Tree();
~Tree();
// Public API
Entry& CreateEntry();
void DoSomething();
};
// Implementation of Tree
#include "Tree.h"
#include "Entry.h"
#include "EntryImpl.h"
#include <vector>
#include <memory>
// Implement the forward-declared class
class TreeImpl
{
public:
TreeImpl() { }
~TreeImpl() { }
std::vector<std::unique_ptr<Entry>> m_Entries;
};
Tree::Tree() : m_Impl(new TreeImpl()) { }
Tree::~Tree() { delete m_Impl; }
Entry& Tree::CreateEntry()
{
// 5. Any constructor parameters can be passed to the private EntryImpl
// class and is therefore hidden from the client.
auto entry = std::make_unique<Entry>(new EntryImpl(/* construction params */));
Entry& entryRef = *entry;
// Move it into our own collection
m_Impl->m_Entries.push_back(std::move(entry));
return entryRef;
}
void Tree::DoSomething()
{
for (const auto& entryPtr : m_Impl->m_Entries)
{
// 6. Can access the implementation from any implementation
// code without modifying the Entry or EntryImpl class.
EntryImpl& entry = EntryImpl::Get(*entryPtr);
entry.DoSomething();
}
}
Entry
的构造函数隐藏了EntryImpl
的构造参数。 (5)EntryImpl
而无需更改Entry
或EntryImpl
的文件。 (6)std::unique_ptr<Entry>
一起使用,无需特殊的解除分配器。我的问题完全在于软件设计。是否有任何替代方法可能对我的方案更好?或者只是我忽略的方法。
答案 0 :(得分:1)
现在这几乎是一个Code Review问题,因此您可能需要考虑在CodeReview.SE上发布此问题。此外,它可能不适合StackOverflow的特定问题的哲学与特定答案,没有讨论。尽管如此,我还是试着提出另一种选择。
Entry(EntryImpl* impl) : m_Impl(impl) { }
// 2. Public destructor
~Entry() { delete m_Impl; }
正如OP已经说过的那样,这些函数都不应该由库的用户调用。例如,如果EntryImpl
具有非平凡的析构函数,析构函数将调用未定义的行为。
在我看来,防止用户构建新的Entry
对象并没有多大好处。在OP之前的一种方法中,Entry
的构造函数都是私有的。使用OP的当前解决方案,库用户可以编写:
Entry e(0);
这会创建一个无法合理使用的对象e
。请注意Entry
应该是不可复制的,因为它拥有数据成员指针指向的对象。
但是,无论类Entry
的定义如何,库用户始终可以使用指针创建引用任何Entry
对象的对象。 (这是针对从树中返回Entry&
的原始实现的参数。)
据我了解OP的意图,Entry
对象使用指向&#34;扩展&#34;它自己的存储到堆上的一些固定内存:
class Entry final
{
private:
EntryImpl* const m_Impl;
由于它是const
,因此您无法重置指针。 Entry
个对象和EntryImpl
个对象之间也存在一对一的关系。但是,库接口必须处理EntryImpl
指针。这些本质上是从库实现传递给库用户的。 Entry
类本身似乎仅用于建立Entry
和EntryImpl
对象之间的一对一关系。
我仍然不完全清楚Entry
和Tree
之间的关系是什么。似乎每个Entry
必须属于Tree
,这意味着Tree
对象应该拥有从中创建的所有条目。这反过来意味着无论库用户从Tree::AddEntry
获得什么,都应该是树所拥有的条目上的视图 - 即指针。有鉴于此,您应该考虑以下解决方案。
如果您可以在库实现和库用户之间共享vtable,则此方法(仅)可用。如果不是这种情况,您可以使用不透明指针而不是具有虚函数的接口来实现类似的方法。这甚至允许将库的接口定义为C API(参见Hourglass interfaces for C++ APIs)。
让我们来看看这些要求的经典解决方案:
// interface headers:
class IEntry // replacement for `Entry`
{
public:
// public API as virtual functions
};
class Tree
{
// [implementation]
public:
IEntry* AddEntry();
void DoSomething();
};
// implementation headers:
class EntryImpl : public IEntry
{
// implementation
};
// implementation of `Tree::AddEntry` returns an `EntryImpl*`
如果条目句柄(IEntry*
)不拥有它引用的条目,则此解决方案很有用。通过从IEntry*
转换为EntryImpl*
,库可以与条目的更多私有部分进行通信。甚至可以为库分隔EntryImpl
与Tree
的第二个界面。就我所见,这种方法不需要课程之间的友谊。
请注意,稍微好一点的解决方案可能是让类EntryImpl
实现概念而不是接口,并将EntryImpl
个对象包装到实现虚拟的适配器中功能。这允许将EntryImpl
类重用于不同的接口。
通过上述解决方案,库用户可以处理指针:
Tree myTree;
auto myEntry = myTree.AddEntry();
myEntry->SomeFunction();
要记录此指针不拥有它指向的对象,您可以使用已命名的&#34;世界上最愚蠢的智能指针&#34;。本质上,一个原始指针的轻量级包装器,作为一种类型,表示它不拥有它指向的对象:
class Tree
{
// [implementation]
public:
non_owning_pointer<IEntry> AddEntry();
void DoSomething();
};
如果要允许用户销毁条目,则应将其从树中删除。否则,您必须明确处理已销毁的条目,例如在TreeImpl::DoSomething
。此时,我们开始为条目重建资源管理系统;第一步通常是破坏。但是,库用户可能对其条目的生命周期有各种要求。如果您只是返回shared_ptr
,那可能是不必要的开销;如果您返回unique_ptr
,则库用户可能必须将unique_ptr
包裹在shared_ptr
中。即使这些解决方案不会对性能产生太大影响,但从概念的角度来看,我认为它们很奇怪。
因此,我认为对于接口,你应该坚持最常用的管理生命的方式,这是(据我所知),类似于组合手册&#34;新&#34;和&#34;删除&#34;调用。我们不能直接使用这些语言功能,因为它们也处理内存。
从树中删除条目需要同时了解条目和树。也就是说,要么既提供销毁功能,要么在每个条目中存储树指针。另一种看待它的方法是:如果您在TreeImpl*
中已经需要EntryImpl
,那么您将免费获得此信息。另一方面,库用户可能已经拥有每个条目的Tree*
。
class Tree
{
// [implementation]
public:
non_owning_pointer<IEntry> AddEntry();
void RemoveEntry(non_owning_pointer<IEntry>);
void DoSomething();
};
(写完之后,这让我想起了迭代器;虽然它们也允许进入 next 条目。)
使用此界面,您可以轻松编写unique_ptr<IEntry, ..>
和shared_ptr<IEntry>
。例如:
namespace detail
{
class UnqiueEntryPtr_deleter {
non_owning_pointer<Tree> owner;
public:
UnqiueEntryPtr_deleter(Tree* t) : owner{t} ()
void operator()(IEntry* p) { owner->RemoveEntry(p); }
};
}
using unique_entry_ptr = std::unique_ptr<IEntry, UniqueEntryPtr_deleter>;
auto AddEntry(Tree& t) // convenience function
{ return unique_entry_ptr{ t.AddEntry(), &t }; }
同样,您可以创建一个对象,其中unique_ptr
为条目,shared_ptr
为Tree
所有者。这可以防止引用死树的Entry*
生命周期问题。
当然,使用多态可以轻松地从库中的IEntry*
到EntryImpl*
。我们能否解决PIMPL方法的问题?是的,要么通过友谊(如在OP中),要么通过提取(副本)PIMPL的功能:
class EntryImpl;
class Entry
{
EntryImpl* pimpl;
public:
EntryImpl const* get_pimpl() const;
EntryImpl* get_pimpl();
};
这看起来不太好,但是由用户编译的库部分必须提取该指针(例如,用户的编译器可以为{{选择不同的内存布局) 1}}对象)。只要Entry
是不透明的指针,就可以说EntryImpl
的封装没有被违反。事实上,Entry
可以很好地封装。