Подтвердить что ты не робот

Как правильно вернуть коллекцию 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, но мне интересно, если есть более элегантный и эффективный способ или общая передовая практика о том, как вернуть такие коллекции.

4b9b3361

Ответ 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;
}