Каков правильный способ разоблачения ресурсов, принадлежащих классу?
Скажем, у меня есть библиотека, которая имеет класс Document
. Экземпляр Document
может иметь несколько экземпляров Field
. Field
имеет несколько подклассов (например, IntegerField
и StringField
), и даже пользователь API может подклассифицировать его и предоставить экземпляры подкласса на Document
(допустим, пользователю разрешено создавать пользовательский тип данных для хранения в поле).
Я хотел бы предоставить экземпляры Field
, принадлежащие Document
, через API таким образом, чтобы пользователи могли взаимодействовать с ними, но без передачи права собственности.
Каков правильный способ сделать это?
Я думал о:
- Предоставление ссылки
const std::unique_ptr<Field>&
- это кажется довольно уродливым
- Предоставление простого указателя
Field*
- это не так, потому что пользователь может быть уверен, удастся ли ему удалить экземпляр или нет.
- Использование
std::shared_ptr
вместо этого - это плохо, потому что владение не действительно разделяется.
Например,
class Document {
private:
std::map<std::string, std::unique_ptr<Field> > fields;
// ...
public:
// ...
// How is this done properly?
const std::unique_ptr<Field> &field(const std::string &name) {
return fields[name];
}
}
Я с нетерпением жду ваших предложений.
(Я также приветствую советы об альтернативных подходах, например, что предложил @Fulvio.)
Ответы
Ответ 1
Как ответили другие специалисты с технической точки зрения, я хотел бы указать вам на другой подход и пересмотреть ваш дизайн. Идея состоит в том, чтобы попытаться уважать Закон Деметры и не предоставлять доступ к субкомпонентам объекта. Это немного сложнее, и без конкретного примера я не могу предоставить много деталей, но попытаюсь представить классную книгу, составленную из страниц. Если я хочу напечатать одну, две или более страницы книги, с вашим текущим дизайном я могу сделать:
auto range = ...;
for( auto p : book.pages(range) )
{
p->print();
}
соблюдая Деметру, вы будете
auto range = ...;
book.print( /* possibly a range here */ );
это наклон к лучшему инкапсуляции, так как вы не полагаетесь на внутренние детали книжного класса, и если его внутренняя структура изменяется, вам не нужно ничего делать с вашим кодом клиента.
Ответ 2
Я бы вернул Bar&
(возможно, с помощью const
).
Пользователю нужно будет понять, что ссылка будет недействительной, если элемент будет удален с карты, но это будет так, как вы делаете, из-за модели с одним владельцем.
Ответ 3
Я обычно возвращаю ссылки на данные, а не ссылку на unique_ptr
:
const Bar &getBar(std::string name) const {
return *bars[name];
}
Если вы хотите вернуть пустой элемент, вы можете вернуть необработанный указатель (и nullptr
в случае пустого). Или еще лучше вы можете использовать boost::optional
(std::optional
в С++ 14).
Если существует вероятность того, что эта ссылка сохранится дольше, чем владелец (многопоточная среда), я использую shared_ptr
внутри себя и возвращаю weak_ptr
в методах доступа.
Ответ 4
Я бы вернул std::weak_ptr< Bar >
(если С++ 11) или boost::weak_ptr
(если не С++ 11). Это делает его явным, что это куча памяти и не рискует обмануть ссылку на несуществующую память (например, Bar &
). Это также делает владение явным.
Ответ 5
Мне вообще не нравится раздавать ссылки на внутренние члены. Что произойдет, если другой поток изменит его? Вместо этого, если я не могу передать копию, я предпочитаю более функциональный подход. Что-то вроде этого.
class Foo {
private:
std::map<std::string, std::unique_ptr<Bar> > bars;
// ...
public:
// ...
template<typename Callable> void withBar(const std::string& name, Callable call) const {
//maybe a lock_guard here?
auto iter = bars.find(name);
if(iter != std::end(bars)) {
call(iter->second.get());
}
}
}
Таким образом, собственная внутренняя структура никогда не "оставляет" принадлежащий ей класс, а владелец может контролировать инварианты. Также может иметь тонкости, например, сделать код noop, если запрошенная запись не существует. Может использовать его как,
myfoo.withBar("test", [] (const Bar* bar){
bar->dostuff();
});
Ответ 6
Если возможно, я попытался бы вообще не подвергать внутренности Document
, но при этом я не вернул бы const Field&
, или если вам нужно, чтобы он был нулевым a const Field*
. Оба ясно показывают, что Document
сохраняет право собственности. Также сделайте сам метод const
.
У вас может быть версия non const
метода, которая возвращает Field&
или Field*
, но я бы избегал этого, если вы можете.
Ответ 7
Это еще не рассмотрено, я думаю: верните прокси-объект с тем же интерфейсом.
Владелец прокси-объекта имеет право собственности на этот прокси-объект.
Прокси-сервер содержит любую ссылку (возможно, слабую) на объект, на который ссылается.
Когда объект будет удален, вы можете вызвать некоторую ошибку.
У объекта eproxy нет права собственности.
Возможно, это уже упоминалось. Я не очень хорошо знаком с С++.