【问题标题】:std::span as a base class for std::vector [closed]std::span 作为 std::vector 的基类 [关闭]
【发布时间】:2021-12-02 01:45:55
【问题描述】:

我目前正在开发一个类似于std::vector 的自定义 C++ 容器库,但我也希望包含std::span 的功能。特别是,我希望能够编写能够接收std::span-like 参数,也可以使用 std::vector-like 参数。

我可以做的是构造一个类,比如my_vector,以及另一个可以从类my_vector 转换的类my_span。这就是 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 实例。向上转换比构造函数更自然。

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

【问题讨论】:

  • Stack Overflow 是一个获取意见的好地方。它更像是一个具体的答案。
  • 我不喜欢基类是派生类的friend。随着更多派生类的添加,它不能很好地扩展。 protected 通常是更好的解决方案。
  • 我个人不会继承,而是为my_vector 提供隐式转换运算符到my_span。是的,您正在创建一个新对象,但如果 my_span 只是成为一个指针和一个大小,那将非常便宜。您还可以将 span 与 vector 分离,正如您所指出的,您已经必须向 span 添加一个成员,而 span 工作不需要该成员。

标签: c++ stl containers library-design


【解决方案1】:

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

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

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

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

最重要的是,数据容器通常将数据视为其中的一部分,而数据视图则不会。因此,获取改变 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};

里面有一个悬空引用。

【讨论】:

  • const 注释中,您可能同时拥有operator span&lt;T&gt;();operator span&lt;const T&gt;() const;
  • @Caleth 在这种情况下,我通常让span 负责转换。它检查传入对象的 .data() 返回指针和 .size() const 返回整数方法,然后假定如果两者都存在,则传入对象具有可以包装的连续缓冲区。这也允许span foo = some_vec; 推断其跨度类型。
  • 哦,是的,我的意思是你可以从 const container 获得 span&lt;const&gt; 并带有适当的重载
猜你喜欢
  • 2021-03-02
  • 2020-07-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-15
  • 2015-10-11
  • 2017-04-10
  • 1970-01-01
相关资源
最近更新 更多