注意:我已经重写了问题,以指定我的意图更清晰,并使其更短。
我正在设计一个库的一部分,它有一些要求:
为了实现这一点,我使用了pimpl习惯用法。
我正在创建一种实例化条目树的方法,并且用户可以在实例化树后为每个实体添加其他行为。稍后,库的其他部分将使用该树执行某些操作。树中的条目不必在内存中复制或移动,分配后,即使更改了树中的父级,它们的内存地址仍保持不变。
由于其他部分需要访问该实现,因此需要某种方式来访问它,同时最好将其限制为客户端代码。
我在最初的问题中描述了多种方法,但是现在我将介绍已实现的方法,我认为这可能是实现此目标的最佳方法之一。
目前的方法
Entry.h
// 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...
};
EntryImpl.h
// 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();
};
树.cpp
// 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>
一起使用,不需要特殊的解除分配器。 缺点
我的问题仅涉及软件设计。有没有其他替代方法可能对我的情况更好?或者只是我忽略的方法。
最佳答案
现在,这几乎是一个代码审查问题,因此您可能需要考虑将此问题发布在CodeReview.SE上。另外,它可能不适合StackOverflow的特定问题,没有特定答案的哲学。尽管如此,我将尝试提出一个替代方案。
对《任择议定书》方法的(细节)进行分析和批评
Entry(EntryImpl* impl) : m_Impl(impl) { }
// 2. Public destructor
~Entry() { delete m_Impl; }
正如OP已经指出的那样,库用户不应调用这些函数。例如,如果
EntryImpl
具有非平凡的析构函数,则析构函数调用Undefined Behavior。在我看来,阻止用户构造新的
Entry
对象没有太大的好处。在OP的先前方法之一中,Entry
的构造函数都是私有(private)的。使用OP的当前解决方案,库用户可以编写:Entry e(0);
这会创建无法合理使用的对象
e
。请注意,Entry
应该不可复制,因为它拥有数据成员指针指向的对象。但是,不管类
Entry
的定义如何,库用户始终可以使用指针创建引用任何Entry
对象的对象。 (这是反对从树中返回Entry&
的原始实现的参数。)据我了解OP的意图,
Entry
对象使用指针将其自身的存储“扩展”到堆上的某些固定内存中:class Entry final
{
private:
EntryImpl* const m_Impl;
由于它是
const
,因此无法重置该指针。 Entry
对象和EntryImpl
对象之间也存在一对一的关系。但是,库接口(interface)必须处理EntryImpl
指针。这些实际上是从库实现传递给库用户的。 Entry
类本身似乎仅用于在Entry
和EntryImpl
对象之间建立一对一关系。对我来说,尚不清楚
Entry
和Tree
之间的关系是什么。似乎每个Entry
必须属于一个Tree
,这意味着Tree
对象应该拥有从其创建的所有条目。反过来,这意味着无论库用户从Tree::AddEntry
获得什么,都应该是该树所拥有条目(即指针)的 View 。因此,您应考虑以下解决方案。一种使用多态的方法
仅当您可以在库实现和库用户之间共享vtable时,此方法才有效。如果不是这种情况,则可以使用不透明指针而不是带有虚函数的接口(interface)来实现类似的方法。这甚至允许将库的接口(interface)定义为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*
,库可以与条目的更多私有(private)部分进行通信。该库甚至还有第二个接口(interface),用于将EntryImpl
与Tree
分开。据我所知,这种方法不需要类之间的友谊。请注意,稍微更好的解决方案可能是让
EntryImpl
类实现概念而不是接口(interface),然后将EntryImpl
对象包装到实现虚拟功能的适配器中。这允许将EntryImpl
类重用于其他接口(interface)。通过上述解决方案,库用户可以处理指针:
Tree myTree;
auto myEntry = myTree.AddEntry();
myEntry->SomeFunction();
要证明此指针不拥有其指向的对象,可以使用被称为“世界上最笨的智能指针”的东西。本质上,原始指针的轻量级包装(作为一种类型)表示它不拥有其指向的对象:
class Tree
{
// [implementation]
public:
non_owning_pointer<IEntry> AddEntry();
void DoSomething();
};
如果要允许用户破坏条目,则应将其从其树中删除。否则,您必须明确处理已破坏的条目,例如在
TreeImpl::DoSomething
中。至此,我们开始为条目重建资源管理系统。第一步通常是销毁。但是,库用户可能对其条目的生存期有各种要求。如果仅返回shared_ptr
,则可能会产生不必要的开销。如果返回unique_ptr
,则库用户可能必须将该unique_ptr
包装在shared_ptr
中。即使这些解决方案对性能的影响不大,但从概念上讲,我还是认为它们很奇怪。因此,我认为对于该接口(interface),您应该坚持最通用的生命周期管理方法(据我所知),类似于手动“new”和“delete”调用的组合。我们不能直接使用这些语言功能,因为它们也处理内存。
从其树中删除条目需要具备以下两项知识:条目和树。也就是说,您既可以提供销毁功能,也可以在每个条目中存储一个树指针。另一种查看方式是:如果您已经需要
TreeImpl*
中的EntryImpl
,则可以免费获得它。另一方面,库用户可能已经具有每个条目的Tree*
。class Tree
{
// [implementation]
public:
non_owning_pointer<IEntry> AddEntry();
void RemoveEntry(non_owning_pointer<IEntry>);
void DoSomething();
};
(写完之后,这让我想起了迭代器;尽管它们也允许进入下一个条目。)
使用此接口(interface),您可以轻松编写
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*
的生命周期问题涉及死树。在PIMPL方法中提升抽象
当然,使用多态性可以轻松地从库中的
IEntry*
转换为EntryImpl*
。我们也可以用PIMPL方法解决问题吗?是的,可以通过友谊(如在OP中),也可以通过提取PIMPL(的副本)的函数进行:class EntryImpl;
class Entry
{
EntryImpl* pimpl;
public:
EntryImpl const* get_pimpl() const;
EntryImpl* get_pimpl();
};
这看起来不太好,但是用户编译的库部分必须提取该指针(例如,用户的编译器可以为
Entry
对象选择不同的内存布局)。只要EntryImpl
是一个不透明的指针,就可以说Entry
的封装没有违反。实际上,EntryImpl
可以被很好地封装。