0

我目前正在开发一个自定义的 C++ 容器库,std::vectorstd::span类似于- 类似的论点。std::spanstd::vector

我能做的是构造一个类,比如说my_vector,和另一个my_span可以从类转换的类my_vector。这就是 STL 所做的,我知道模仿标准库通常是个好主意。但是我有这个想法,my_span基本上是my_vector不拥有内存的,因此可以使用继承来实现这两个类。这是它在代码中的样子。

class my_vector;

class my_span {
        private:
        /* span sees [data_ + start_, data_ + stop_) */
        T* data_;
        size_t start_; 
        size_t stop_;
        friend class my_vector;
        public:
        /* Member functions operating on non-owning memory */
};

class my_vector : public my_span {
        private:
        size_t cap_;
        public:
        /* Member functions like resize, push_back, etc. */
};

现在我的同事基于以下原因拒绝了这个想法。公平地说,我对他的反对意见的表述可能并不忠实。

  1. 在实际容器之前定义跨度是违反直觉的。
  2. 当派生类扩展时使用继承,但该类my_vector的条件是其成员start_始终为0. (有一些原因迫使指针data_总是指向分配内存的开头。这就是为什么我不能只使用指针和跨度的长度。)

另一方面,我相信这种设计有以下好处。

  1. 如果你仔细想想,my_vector仍然是“是一个” my_span。它只是一个my_span拥有内存并且可以改变大小的东西。
  2. 每个在非拥有内存上操作的成员函数只能声明和实现一次;该类my_vector自动继承它。
  3. my_vector用作my_span,您无需创建新my_span实例。向上转换比构造函数更自然。

我还没有看到遵循这种模式的设计,所以我想获得更多关于这是否是一个好的设计的意见。

4

1 回答 1

4

LSP声明指向派生类的引用或指针应该遵守对基类的引用的所有不变量。

这必须是每个操作。这比你想象的要难。

替换跨度的引用缓冲区是一个完美的跨度操作。对派生向量的跨度父组件这样做是有毒的!实际上,您最终不得不将您可以做的事情限制在一个跨度上以使其工作,从而导致一个残缺的跨度类型,或者一个不安全的组合。

这里更好的选择可能是从向量到跨度的隐式转换(但不是相反,这应该是显式的,因为它很昂贵)。

最重要的是,数据容器通常认为数据是其中的一部分,而数据视图则不然。因此,获取改变 span 内容的开始/结束迭代器是 const,而对向量执行相同操作则不是!

template<class T>
struct span {
  T* data = nullptr;
  std::size_t length = 0;
  T* begin() const { return data; }
  T* end() const { return data+length; }
};
template<class T>
struct vector {
  T* data = nullptr;
  std::size_t length = 0;
  std::size_t capacity = 0;
  T const* begin() const { return data; }
  T const* end() const { return data+length; }
  T * begin() { return data; }
  T * end() { return data+length; }
};

另一个微妙的区别。

对于 span-likes(如数组视图),我遵循的规则是它们负责转换自。他们将从

  1. 原始 C 数组。
  2. 初始化器列表。(警告:有些危险)
  3. 任何具有.data()返回指针(指向兼容类型)和.size()返回整数值的对象。请注意,我们正在做指针运算,因此 compatible 是“与 const volatile 相同”。

他们从以上所有内容中推断出他们的类型(使用模板类推断功能)。

规则 #3 “免费”捕获标准向量和标准数组以及标准字符串。

规则 #2 允许

void foo( span<const flag> );
foo( {flag::a, flag::b} );

初始化列表的危险是:

span<int> sp = {1,2,3};

里面有一个悬空的参考。

于 2021-10-13T15:12:12.590 回答