Как правильно вернуть коллекцию unique_ptr
После изменения кода для использования уникальных указателей я наткнулся на то, как вернуть коллекцию объектов клиенту. В общем, я хочу передать объекты в качестве ссылок или указатели без прав. Но если у меня есть набор объектов, я не могу просто вернуть ссылку на него.
В качестве примера у меня есть простой класс с коллекцией объектов, которые все создаются один раз и не изменяются впоследствии.
using ObjectUPtr = std::unique_ptr<Object>;
class MyClass
{
public:
const std::vector<Object*>& GetObjectsOldStyle() const
{
return mObjectsOldStyle;
}
const std::vector<VObjectUPtr>& GetObjectsNewStyleA() const
{
// I don't like that: The client should not see the unique_ptr ...
return mObjectsNewStyle;
}
std::vector<VObject*> GetObjectsNewStyleB() const
{
// Ok, but performance drops
std::transform(...); // Transform the collection and return a copy
}
const std::vector<VObject*>& GetObjectsNewStyleC() const
{
// Ok, only copied once, but two variables per collection needed
// Transform the collection and cache in a second vector<Object*>
std::transform(...);
}
std::vector<Object*> mObjectsOldStyle; // old-style owning pointers here
std::vector<ObjectUPtr> mObjectsNewStyle; // how I want to do it today
}
Сегодня я обычно предпочитаю GetObjectsNewStyleB, но мне интересно, если есть более элегантный и эффективный способ или общая передовая практика о том, как вернуть такие коллекции.
Ответы
Ответ 1
Я бы рекомендовал создать свой собственный класс итератора. Затем создайте функции начала и конца элемента. Вы даже можете перегрузить оператор разыменования, чтобы возвращать ссылки, а не указатели (если ваши указатели не равны нулю). Это может начаться примерно так:
class iterator :
public std::iterator<std::random_access_iterator_tag, Object>
{
public:
Object& operator*() const { return **base; }
Object* operator->() const { return &**base; }
iterator& operator++() { ++base; return *this; }
// several other members necessary for random access iterators
private:
std::vector<ObjectUPtr>::iterator base;
};
Это немного утомительно, реализуя стандартный итератор, но я думаю, что это, безусловно, самое идиоматическое решение. Как упоминалось в комментариях, библиотека Boost.Iterator, в частности boost::iterator_facade
, может быть использована для облегчения некоторой скуки.
Ответ 2
Вам не нужно возвращать коллекцию, тем самым нарушая инкапсуляцию класса контейнера. Существуют и другие варианты.
Я бы пошел с шаблоном Enumerator/Receiver (я не знаю, является ли это фактическим именем для этого шаблона).
Основная идея заключается в том, чтобы клиент вашего API реализовал интерфейс, который в принципе получает объекты из контейнера один за другим.
Он выглядит следующим образом:
class Receiver {
public:
virtual void receive(const Object& object) = 0;
};
class Container {
public:
void enumerate(Receiver& receiver) const {
for (auto&& obj : m_objects) {
receiver.receive(*obj);
}
}
private:
std::vector<ObjectUPtr> m_objects;
};
Затем реализуем интерфейс Receiver:
class ReceiverImpl : public Receiver {
public:
virtual void receive(const Object& object) {
// do something with object
}
};
и контейнер перечислит объекты получателю:
Container container;
ReceiverImpl receiver;
container.enumerate(receiver);
Смотрите живой пример.
Кроме того, вы даже можете сделать контейнер потокобезопасным, просто добавив блокировку/разблокировку мьютекса в Container::enumerate
, и клиент даже не заметит!
Наконец, вы можете заменить аргумент приемника в Container::enumerate
аргументом шаблона, чтобы избавиться от служебных вызовов во время выполнения вызовов виртуальных функций.
Ответ 3
Если вы используете boost, я предпочитаю tranform_iterator
over iterator_facade
в таких случаях.
Чтобы получить более функциональный стиль, я разделил код на две части:
- создать класс, который может содержать два указателя в качестве диапазона (позволяет диапазон, основанный на циклах).
- создайте функцию, которая принимает лямбда в качестве шага преобразования и возвращает диапазон (чтобы скрыть
transform_iterator
).
Таким образом, шаг преобразования выполняется при разыменовании итератора.
Пример кода
#include <boost/iterator/transform_iterator.hpp>
#include <memory>
#include <vector>
#include <algorithm>
#include <stdio.h>
using namespace boost;
using namespace std;
template <typename Iterator>
class Range {
public:
Range(Iterator begin, Iterator end) : b(begin), e(end) { }
Range(const Range &r) = default;
Range(Range &&r) = default;
Range &operator=(const Range &r) = default;
Range &operator=(Range &&r) = default;
template <typename Container>
Range(Container &c) : b(c.begin()), e(c.end()) { }
Iterator begin() { return b;}
Iterator begin() const { return b; }
Iterator end() { return e;}
Iterator end() const { return e; }
Iterator b;
Iterator e;
};
template <typename Container, typename TransformFunc>
Range<transform_iterator<TransformFunc, typename Container::iterator>>
transform(Container &c, TransformFunc f) {
using namespace boost;
using cont_it = typename Container::iterator;
using iterator = transform_iterator<TransformFunc, cont_it>;
iterator b = iterator(c.begin(), f), e = iterator(c.end(), f);
Range<iterator> r(b,e);
return r;
}
int main(int, char **) {
vector<unique_ptr<int>> foo;
for (int i = 0; i < 10; i++) {
foo.push_back(unique_ptr<int>(new int(10)));
}
auto f = [](unique_ptr<int> &i) { return i.get(); };
for (auto *i : transform(foo, f) ) {
printf("%p ", i);
}
return 0;
}