Const-correct accessor к вектору указателей без передачи права собственности в абстрактном интерфейсе

Я разрабатываю библиотеку с нуля и хочу как можно лучше получить публичный API. Я хочу, чтобы компилятор кричал на меня за неправильное использование. Поэтому я наложил на себя следующие правила:

  • истинная (т.е. глубокая и полная) константная корректность во всей библиотеке

    Все вещи (локальные переменные, переменные-члены, функции-члены), которые не должны изменяться, объявляются const. Эта константа должна распространяться на все вложенные элементы и типы.

  • Явное и выразительное владение

    В соответствии с Основными правилами С++ я определяю это как (iff в математическом смысле if и только if):

    • аргументы функции unique_ptr<T> или T&&, если функция потребляет ее (т.е. получает право собственности)
    • аргументы функции shared_ptr<const T> или T const&, если функция только читает ее
    • аргументы функции shared_ptr<T> или T&, если функция изменяет его, не принимая права собственности
    • значения возврата unique_ptr<T> или T, если функция передает право собственности вызывающему абоненту
    • Возвращаемые значения shared_ptr<const T> или T const&, если вызывающий объект должен только читать его (хотя вызывающий может построить его копию - данный T можно скопировать)
    • никакие функции не должны возвращать shared_ptr<T>, T& или T* (так как это позволяло бы неконтролируемые побочные эффекты, которые я стараюсь избегать по дизайну)
  • скрытые подробности реализации

    В настоящее время я собираюсь с абстрактными интерфейсами с заводами, возвращающими реализацию как unique_ptr<Interface>. Хотя, я открыт для альтернативных шаблонов, которые решают мою проблему, описанную ниже.

Я не забочусь о виртуальных поисках таблиц и хочу избегать динамических бросков всеми способами (я вижу их как запах кода).


Теперь, учитывая два класса A и B, где B принадлежит переменное число A s. Кроме того, мы выполняем B -implementation BImpl (реализация A, вероятно, не используется здесь):

class A
{};

class B {
 public:
  virtual ~B() = default;
  virtual void addA(std::unique_ptr<A> aObj) = 0;
  virtual ??? aObjs() const = 0;
};

class BImpl : public B {
 public:
  virtual ~BImpl() = default;
  void addA(std::unique_ptr<A> aObj) override;
  ??? aObjs() const override;

 private:
  std::vector<unique_ptr<A>> aObjs_;
};

Я привязался к возвращаемому значению B getter к вектору A s: aObjs().
Он должен предоставить список A как значения, доступные только для чтения, без передачи права собственности (правило 2.5 выше с константной корректностью) и по-прежнему предоставлять вызывающему абоненту легкий доступ ко всем A s, например. для использования в диапазоне for или стандартных алгоритмах, таких как std::find.

Я предложил следующие опции для ???:

  • std::vector<std::shared_ptr<const A>> const&

    Мне пришлось бы создавать новый вектор каждый раз, когда я вызываю aObjs() (я мог бы кэшировать его в BImpl). Это кажется не только неэффективным и ненужным сложным, но и очень субоптимальным.

  • Замените aObjs() парой функций (aObjsBegin() и aObjsEnd()), пересылающих константный итератор BImpl::aObjs_.

    Подождите. Мне нужно сделать это unique_ptr<A>::const_iterator a unique_ptr<const A>::const_iterator, чтобы получить мою любимую константу. Опять неприятные броски или промежуточные объекты. И пользователь не мог легко использовать его на основе диапазона for.

Какое очевидное решение мне не хватает?


Edit:

  • B должен всегда иметь возможность модифицировать A, который удерживается, поэтому объявление aObjs_ как vector<std::unique_ptr<const A>> не является параметром.

  • Пусть B придерживается концепции итератора для итерации по A s, не является параметром, так как B будет содержать список C и конкретный D (или none).

Ответ 1

С range-v3 вы можете сделать

template <typename T>
using const_view_t = decltype(std::declval<const std::vector<std::unique_ptr<T>>&>() 
                        | ranges::view::transform(&std::unique_ptr<T>::get)
                        | ranges::view::indirect);

class B
{
public:
    virtual ~B() = default;
    virtual void addA(std::unique_ptr<A> a) = 0;    
    virtual const_view_t<A> getAs() const = 0;
};

class D : public B
{
public:
    void addA(std::unique_ptr<A> a) override { v.emplace_back(std::move(a)); }
    const_view_t<A> getAs() const override {
        return v | ranges::view::transform(&std::unique_ptr<A>::get)
                 | ranges::view::indirect;
    }

private:
    std::vector<std::unique_ptr<A>> v;
};

И затем

for (const A& a : d.getAs()) {
    std::cout << a.n << std::endl;   
}

Демо

Ответ 2

Вместо того чтобы пытаться напрямую вернуть вектор, вы можете вернуть оболочку вектора, который позволяет вам получать доступ к контенту только с помощью указателей const. Это может показаться сложным, но это не так. Просто создайте тонкую оболочку и добавьте функцию begin() и end(), чтобы разрешить итерацию:

struct BImpl : B {
    virtual ~BImpl() = default;
    void addA(std::unique_ptr<A> aObj) override;

    ConstPtrVector<A> aObjs() const override {
        return aObjs_;
    }

private:
    std::vector<unique_ptr<A>> aObjs_;
};

ConstPtrVector будет выглядеть следующим образом:

template<typename T>
ConstPtrVector {
    ConstPtrVector(const std::vector<T>& vec_) : vec{vec_} {}

    MyConstIterator<T> begin() const {
        return vec.begin();
    }

    MyConstIterator<T> end() const {
        return vec.end();
    }

private:
    const std::vector<T>& vec;
};

И вы можете реализовать MyConstIterator таким образом, чтобы возвращать указатели как const:

template<typename T>
struct MyConstIterator {
    MyConstIterator(std::vector<unique_ptr<T>>::const_iterator it_) : it{std::move(it_)} {}

    bool operator==(const MyConstIterator& other) const {
        return other.it == it;
    }

    bool operator!=(const MyConstIterator& other) const {
        return other.it != it;
    } 

    const T* operator*() const {
        return it->get();
    }

    const T* operator->() const {
        return it->get();
    }

    MyConstIterator& operator++() {
        ++it;
        return *this;
    }

    MyConstIterator& operator--() {
        --it;
        return *this;
    }

private:
    std::vector<unique_ptr<T>>::const_iterator it;
};

Конечно, вы можете обобщить этот итератор и обертку, реализуя векторный интерфейс.

Затем, чтобы использовать его, вы можете использовать цикл на основе цикла или классический цикл на основе итератора.

BTW: Нет ничего плохого в том, что вы не владеете необработанными указателями. Пока они все еще не владеют. Если вы хотите избежать ошибок из-за необработанных указателей, посмотрите observer_ptr<T>, это может быть полезно.

Ответ 3

template<class It>
struct range_view_t {
  It b{};
  It e{};
  range_view_t(It s, It f):b(std::move(s)), e(std::move(f)) {}
  range_view_t()=default;
  range_view_t(range_view_t&&)=default;
  range_view_t(range_view_t const&)=default;
  range_view_t& operator=(range_view_t&&)=default;
  range_view_t& operator=(range_view_t const&)=default;

  It begin() const { return b; }
  It end() const { return e; }
};

здесь мы начинаем с диапазона итераторов.

Мы можем сделать его богаче range_view_t remove_front(std::size_t n = 1)const, bool empty() const, front() и т.д.

Мы можем увеличить его, используя обычные методы, условно добавляя operator[] и size, если It имеет категорию random_access_iterator_tag и делает remove_front молча привязанным n.

Затем, сделав еще один шаг, напишем array_view_t:

template<class T>
struct array_view_t:range_view<T*> {
  using range_view<T*>::range_view;
  array_view_t()=default; // etc
  array_view_t( T* start, std::size_t length ):array_view_t(start, start+length) {}
  template<class C,
    std::enable_if_t
      std::is_same< std::remove_pointer_t<data_type<C>>, T>{}
      || std::is_same< const std::remove_pointer_t<data_type<C>>, T>{},
      , int
    > =0
  >
  array_view_t( C& c ):array_view_t(c.data(), c.size()) {}
  template<std::size_t N>
  array_view_t( T(&arr)[N] ):array_view_t( arr, N ) {}
};

который абстрагирует просмотр содержимого смежного контейнера.

Теперь ваш BImpl возвращает array_view_t< const std::unique_ptr<A> >.

Этот уровень абстракции в основном свободен.


Если этого недостаточно, вы производите стирание случайного доступа T, затем возвращаете range_view_t< any_random_access_iterator<T> >, где в этом случае T есть const std::unique_ptr<A>.

Мы также могли бы стереть семантику собственности и просто быть range_view_t< any_random_access_iterator<A*> > после того, как выбрали адаптер диапазона.

Этот уровень стирания типа не является бесплатным.


Для полного безумия вы можете прекратить использование интеллектуальных указателей или интерфейсов.

Опишите свои интерфейсы, используя стирание типа. Пропустите любой завернутый тип стирания. Почти все использует семантику значений. Если вы потребляете копию, возьмите по значению, а затем перейдите из этого значения. Избегайте постоянных ссылок на объекты. Краткосрочные ссылки являются ссылками или указателями, если они являются необязательными. Они не сохраняются.

Используйте имена вместо адресов и используйте реестр где-нибудь, чтобы получить элементы, когда вы не можете позволить себе использовать значения.