Примечание. Я переформулировал вопрос, чтобы более четко указать свои намерения и сделать его короче.
Я разрабатываю часть библиотеки, которая имеет несколько требований:
- Ни одна из деталей реализации не должна быть видна из общедоступных заголовков.
- Память должна управляться библиотекой.
- Клиент получает доступ к необходимой ему информации через ссылку на дескриптор.
Для этого я использую идиому pimpl.
Я создаю способ создания экземпляра дерева записей, и пользователь может после создания экземпляра дерева добавить дополнительное поведение к каждому объекту. Позже это дерево используется другими частями библиотеки для выполнения некоторых действий. Записи в дереве не нужно копировать или перемещать в память, после выделения их адрес памяти остается фиксированным, даже если родитель в дереве меняется.
Поскольку другим частям необходим доступ к реализации, должен быть какой-то способ получить к ней доступ, предпочтительно ограничивая его клиентским кодом.
У меня было несколько методов, которые я описал в своем первоначальном вопросе, но теперь я собираюсь представить тот, который я реализовал, и я думаю, что это может быть одним из лучших способов добиться этого.
Текущий подход
- Открытый конструктор принимает указатель (владелец) на класс реализации. (1)
- Публичный деструктор. (2)
- Дружба с классом реализации. (3)
- Класс реализации предоставляет статический метод для получения доступа к классу реализации из ссылки на исходный класс. (4)
Запись.ч
// 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>
, не требуя специального делокатора.
Недостатки
- Открытый деструктор позволяет клиентскому коду освобождать память Entry, вызывая почти немедленный сбой.
- Дружба? Хотя большинство проблем, связанных с дружбой, здесь не так заметны.
Мой вопрос касается исключительно дизайна программного обеспечения. Существуют ли какие-либо альтернативные подходы, которые могут быть лучше для моего сценария? Или просто методы, которые я пропускаю.
std::vector<std::unique_ptr<Entry>>
(или, скорее, какова цельEntryManager
)? ПочемуEntry::EntryImpl
константа? Является лиEntryImpl
копируемым, перемещаемым, ..? - person dyp   schedule 30.12.2014EntryImpl
не константа, указатель. Это означает, что базовое хранилище реализации не может быть изменено после создания экземпляра. В настоящее время ни один из классов не может быть скопирован или перемещен. Они будут доступны только потребителю библиотеки в качестве ссылок. --- В основном я работаю с ограничениями, из-за которых никакие детали реализации не могут быть доступны потребителю библиотеки. Кроме того, вся память управляется библиотекой, а все данные, предоставляемые клиенту, представлены в виде дескрипторов, а не реальных объектов. - person Aidiakapi   schedule 30.12.2014Entry::m_Impl
, то есть элемент данных. Члены данныхconst
довольно неприятны, поэтому используются только для специальных целей. -- Я не совсем уверен, что понимаю связь между всей памятью управляет библиотека и они будут отображаться только как ссылки. ЕслиEntryImpl
можно копировать, то вы можете сделатьEntry
тоже копируемым, и, например, не допускать утечки деталей реализации. Кроме того, почему базовое хранилище реализации нельзя изменить после создания экземпляра? Какова связь между объектомEntry
и объектомEntryImpl
, на который он ссылается? - person dyp   schedule 30.12.2014Entry::m_Impl
тоже. Я пометил указатель какconst
, потому что не собираюсь менять его после созданияEntry
. Я мог бы изменить хранилище класса реализации (изменитьEntry::m_Impl
), пометив его как неконстантный, но в моем случае использования это не имело бы смысла. Они не автономные сущности, и нет смысла перемещать или копировать память. По сути, они являются точкой входа для пользователей, чтобы подключить свой собственный код настройки, но пользователи не могут стать владельцами этих классов Entry. - person Aidiakapi   schedule 30.12.2014EntryManagerImpl::m_Entries
мне неясна. Вам нужно просто хранить экземплярыEntry
/EntryImpl
где-то в памяти, или библиотеке нужен список всех (текущих) экземпляровEntryImpl
? Могут ли быть разные объекты типаEntryManager
? Если да, то (чем) отличается их поведение? Библиотека может запросить новыйEntry
черезEntryManager::CreateEntry
. Может ли пользователь запросить уничтожение одного экземпляраEntry
? (Имеет ли это смысл?) - person dyp   schedule 30.12.2014std::vector
, но в реальном сценарии базовое хранилище адаптируется к хранимому содержимому (когда набор данных становится больше, он переключается на более эффективно модифицируемые коллекции). --- Пользователь может запросить (отложенное) удаление, а также изменить, к какомуEntryManager
он принадлежит, но все это делается путем перемещенияstd::unique_ptr<Entry>
, а не путем перераспределения/копирования/перемещения фактическогоEntry
. - person Aidiakapi   schedule 30.12.2014Entry
s (ни функций-членов, ни свободных функций, принимающихEntry&
s). ИспользованиеEntry
определяет, какая форма (представление) подходит для него. Насколько я понимаю, вы можете просто вернуть (не принадлежащий) указатель изEntryManager::CreateEntry
, удаляя большую часть цели классаEntry
. - person dyp   schedule 30.12.2014