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. */
};
现在我的同事基于以下原因拒绝了这个想法。公平地说,我对他的反对意见的陈述可能并不忠实。
- 在实际容器之前定义跨度是违反直觉的。
- 派生的class扩展时使用继承,但是class
my_vector
的条件是其成员start_
永远是0
. (有一些原因迫使指针 data_
始终指向已分配内存的开头。这就是为什么我不能只使用指针和跨度的长度。)
另一方面,我认为这种设计有以下好处。
- 仔细想想,
my_vector
还是“一个”my_span
。它只是一个 my_span
拥有内存并且可以改变大小。
- 每个对非拥有内存进行操作的成员函数只能声明和实现一次; class
my_vector
自动继承它。
- 要将
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(如数组视图)遵循的规则是它们负责转换。他们将从
- 原始 C 数组。
- 初始化程序列表。 (警告:有点危险)
- 具有
.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};
其中有一个悬挂引用。
我目前正在开发一个类似于 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. */
};
现在我的同事基于以下原因拒绝了这个想法。公平地说,我对他的反对意见的陈述可能并不忠实。
- 在实际容器之前定义跨度是违反直觉的。
- 派生的class扩展时使用继承,但是class
my_vector
的条件是其成员start_
永远是0
. (有一些原因迫使指针data_
始终指向已分配内存的开头。这就是为什么我不能只使用指针和跨度的长度。)
另一方面,我认为这种设计有以下好处。
- 仔细想想,
my_vector
还是“一个”my_span
。它只是一个my_span
拥有内存并且可以改变大小。 - 每个对非拥有内存进行操作的成员函数只能声明和实现一次; class
my_vector
自动继承它。 - 要将
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(如数组视图)遵循的规则是它们负责转换。他们将从
- 原始 C 数组。
- 初始化程序列表。 (警告:有点危险)
- 具有
.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};
其中有一个悬挂引用。