std::span 作为 class 的基础 std::vector

std::span as a base class for std::vector

我目前正在开发一个类似于 std::vector 的自定义 C++ 容器库,但我也希望内置 std::span 的功能。特别是,我希望能够编写接受 std::span 类参数的函数,并使用 std::vector 类参数。

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

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. 派生的class扩展时使用继承,但是classmy_vector的条件是其成员start_永远是0 . (有一些原因迫使指针 data_ 始终指向已分配内存的开头。这就是为什么我不能只使用指针和跨度的长度。)

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

  1. 仔细想想,my_vector还是“一个”my_span。它只是一个 my_span 拥有内存并且可以改变大小。
  2. 每个对非拥有内存进行操作的成员函数只能声明和实现一次; class my_vector 自动继承它。
  3. 要将 my_vector 用作 my_span,您不需要创建新的 my_span 实例。向上转换比构造函数自然得多。

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

LSP 声明指向派生 class 的引用或指针应遵守对基 class.

的引用的所有不变量

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

替换 span 的引用缓冲区是一个完美的 cromulant span 操作。对派生向量的 span 父组件这样做是有毒的!实际上,您最终必须限制您可以对跨度执行的操作才能使其正常工作,从而导致跨度类型受损或组合不安全。

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

最重要的是,数据容器通常将数据视为其中的一部分,而数据视图则不然。因此,获取 begin/end 改变跨度内容的迭代器是常量,而对向量执行相同操作则不是!

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() 返回整数值的任何对象。请注意,我们正在进行指针运算,因此兼容“与 const volatile 相同”。

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

规则 #3 “免费”捕获 std 向量、std 数组和 std 字符串。

规则 #2 允许

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

初始化列表的危险是:

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

其中有一个悬挂引用。