在抽象接口中不转移所有权的指针向量的常量正确访问器

Const-correct accessor to vector of pointers without transfer of ownership in abstract interface

我正在从头开始设计一个库,希望 public API 尽可能 。我希望编译器因滥用而对我大喊大叫。因此,我给自己规定了以下规则:

  1. 整个库的真正(即深入和完整)const 正确性

    声明所有事物(局部变量、成员变量、成员函数),预计不会改变const。该常量性应传播到所有嵌套成员和类型。

  2. 明确和表达所有权

    根据 C++ 核心指南,我将其定义为(iff​​ 的数学意义上当且仅当 ):

    1. 函数参数是 unique_ptr<T>T&& iff​​ 函数正在使用它(即取得所有权)
    2. 函数参数是shared_ptr<const T>T const&iff​​函数只读取它
    3. 函数参数是 shared_ptr<T>T& iff​​ 函数正在修改它而不取得所有权
    4. return 值为 unique_ptr<T>T iff​​ 函数将所有权转移给调用者
    5. return 值是 shared_ptr<const T>T const& iff​​ 调用者应该只读取它(尽管调用者可以构造它的副本- 鉴于 T 是可复制的)
    6. 任何函数都不应该 return shared_ptr<T>T&T*(因为它会导致无法控制的副作用,我试图通过设计避免)
  3. 隐藏实现细节

    现在我将使用工厂的抽象接口 return 将实现作为 unique_ptr<Interface>。不过,我愿意接受可以解决下面描述的问题的替代模式。

我不关心虚拟 table 查找,并希望通过各种方式避免动态转换(我认为它们是一种代码味道)。


现在,给定两个 类 AB,其中 B 拥有可变数量的 A。我们还有 B-实现 BImplA 的实现在这里可能没有用):

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_;
};

我坚持使用 Bs getter 的 return 值到 As 的向量:aObjs().
它应该提供 As 的列表作为只读值而不转移所有权(规则 2.5。上面的 const 正确性)并且仍然为调用者提供对所有 As 的轻松访问,例如用于基于范围的 for 或标准算法,例如 std::find.

我为那些 ??? 提出了以下选项:

  1. std::vector<std::shared_ptr<const A>> const&

    每次调用 aObjs() 时我都必须构建一个新向量(我可以将它缓存在 BImpl 中)。这感觉不仅效率低下而且不必要地复杂,而且似乎也不是最理想的。

  2. 用一对函数(aObjsBegin()aObjsEnd())替换aObjs()转发BImpl::aObjs_.

    的常量迭代器

    等等。我需要将 unique_ptr<A>::const_iterator 设为 unique_ptr<const A>::const_iterator 以获得我心爱的 const 正确性。再次令人讨厌的演员表或中间对象。并且用户无法在基于范围的 for.

  3. 中轻松使用它

我缺少什么明显的解决方案?


编辑:

您可以 return 向量的包装器,而不是直接尝试 return 向量,它允许您仅使用 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;
};

并且您可以以将 return 指针作为 const:

的方式实现 MyConstIterator
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;
};

当然,您可以通过实现类似接口的向量来概括此迭代器和包装器。

然后,要使用它,您可以使用基于范围的 for 循环或基于经典迭代器的循环。

顺便说一句:不拥有原始指针并没有错。只要他们仍然不拥有。如果你想避免由于原始指针导致的错误,请查看 observer_ptr<T>,它可能会有用。

有了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;   
}

Demo

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)constbool empty() constfront() 等使它更丰富

我们可以使用通常的技术对其进行扩充,如果 It 具有 random_access_iterator_tag 类别,则有条件地添加 operator[]size,并静默添加 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 return 是 array_view_t< const std::unique_ptr<A> >

这个抽象级别基本上是免费的。


如果这还不够,您键入 T 的擦除随机访问迭代,然后 return a range_view_t< any_random_access_iterator<T> >,在这种情况下 Tconst std::unique_ptr<A>.

我们也可以删除所有权语义,并在放入范围适配器后成为 range_view_t< any_random_access_iterator<A*> >

这种级别的类型擦除不是免费的。


对于彻底的疯狂,您可以停止使用智能指针或接口。

使用类型擦除描述您的接口。传出任何用类型擦除包裹的。几乎所有东西都使用值语义。如果您消费一个副本,按值取用,然后移出该值。避免对对象的持久引用。短期引用是引用,如果它们是可选的,则为指针。这些没有存储。

当您无力使用值时,使用名称而不是地址,并使用某处的注册表来获取项目。