在c++中,内存的管理和限制访问,怎么说都是一个道理简单但实际操作起来麻烦又难的一个问题。前面提到过字符串的string_view,那么对于普通的连续内存有没有好的方法呢?即使是加强一些都行啊。在内存操作过程中,越界和引用失效的内存地址同样是致命的。而在一块比较大的内存中,这又是很容易犯下的错误。c++新版本中有了std::span这个模板类。
这个模板类的基本作用就是对连续内存的管理。安全性是c++标准不断迭代也不断加强的一个重要的方向和目标。可以看std::span看作一种索引,它能够保证传递的连续内存的长度的正确性。这时候儿想一想如果一个数组退化成指针后,传递给函数,长度的控制一定是一个首要的问题。一个不小心,越界的问题就出现了。看一下在c++中的具体的定义:
namespace std {
template
class span {
public:
// 常量与类型
using element_type = ElementType;
using value_type = remove_cv_t;
using size_type = size_t;
using difference_type = ptrdiff_t;
using pointer = element_type*;
using const_pointer = const element_type*;
using reference = element_type&;
using const_reference = const element_type&;
using iterator = /* 由实现定义 */;
using reverse_iterator = std::reverse_iterator;
static constexpr size_type extent = Extent;
// 构造函数、复制与赋值
constexpr span() noexcept;
template
constexpr explicit(extent != dynamic_extent) span(It first, size_type count);
template
constexpr explicit(extent != dynamic_extent) span(It first, End last);
template
constexpr span(type_identity_t (&arr)[N]) noexcept;
template
constexpr span(array& arr) noexcept;
template
constexpr span(const array& arr) noexcept;
template
constexpr explicit(extent != dynamic_extent) span(R&& r);
constexpr span(const span& other) noexcept = default;
template
constexpr explicit(/* 见描述 */)
span(const span& s) noexcept;
~span() noexcept = default;
constexpr span& operator=(const span& other) noexcept = default;
// 子视图
template
constexpr span first() const;
template
constexpr span last() const;
template
constexpr span subspan() const;
constexpr span first(size_type count) const;
constexpr span last(size_type count) const;
constexpr span subspan(
size_type offset, size_type count = dynamic_extent) const;
// 观察器
constexpr size_type size() const noexcept;
constexpr size_type size_bytes() const noexcept;
[[nodiscard]] constexpr bool empty() const noexcept;
// 元素访问
constexpr reference operator[](size_type idx) const;
constexpr reference front() const;
constexpr reference back() const;
constexpr pointer data() const noexcept;
// 迭代器支持
constexpr iterator begin() const noexcept;
constexpr iterator end() const noexcept;
constexpr reverse_iterator rbegin() const noexcept;
constexpr reverse_iterator rend() const noexcept;
private:
pointer data_; // 仅用于阐释
size_type size_; // 仅用于阐释
};
template
span(It, EndOrSize) -> span>>;
template
span(T (&)[N]) -> span;
template
span(array&) -> span;
template
span(const array&) -> span;
template
span(R&&) -> span>>;
}
认真看一下相关的函数,其实都是用来处理数据的头尾以及相关长度的控制的,其实正好体现了定义中的对连续内存的限制管理。
看一个具体的例子:
#include
#include
#include
#include
template [[nodiscard]]
constexpr auto slide(std::span s, std::size_t offset, std::size_t width) {
return s.subspan(offset, offset + width <= s.size() ? width : 0U);
}
template [[nodiscard]]
constexpr bool starts_with(std::span data, std::span prefix) {
return data.size() >= prefix.size()
&& std::equal(prefix.begin(), prefix.end(), data.begin());
}
template [[nodiscard]]
constexpr bool ends_with(std::span data, std::span suffix) {
return data.size() >= suffix.size()
&& std::equal(data.end() - suffix.size(), data.end(),
suffix.end() - suffix.size());
}
template [[nodiscard]]
constexpr bool contains(std::span span, std::span sub) {
return std::search(span.begin(), span.end(), sub.begin(), sub.end()) != span.end();
// return std::ranges::search(span, sub).begin() != span.end();
}
void print(const auto& seq) {
for (const auto& elem : seq) std::cout << elem << ' ';
std::cout << '\n';
}
int main()
{
constexpr int a[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
constexpr int b[] { 8, 7, 6 };
for (std::size_t offset{}; ; ++offset) {
constexpr std::size_t width{6};
auto s = slide(std::span{a}, offset, width);
if (s.empty())
break;
print(s);
}
static_assert(
starts_with( std::span{a}, std::span{a, 4} ) and
starts_with( std::span{a + 1, 4}, std::span{a + 1, 3} ) and
! starts_with( std::span{a}, std::span{b} ) and
! starts_with( std::span{a, 8}, std::span{a + 1, 3} ) and
ends_with( std::span{a}, std::span{a + 6, 3} ) and
! ends_with( std::span{a}, std::span{a + 6, 2} ) and
contains( std::span{a}, std::span{a + 1, 4} ) and
! contains( std::span{a, 8}, std::span{a, 9} )
);
}
运行结果是:
0 1 2 3 4 5
1 2 3 4 5 6
2 3 4 5 6 7
3 4 5 6 7 8
然后再看一个简单的例子,对照着就更好理解了:
#include
#include
int main()
{
constexpr char str[] = "ABCDEF\n";
const std::span sp{str};
for (auto n{sp.size()}; n != 2; --n) {
std::cout << sp.last(n).data();
}
}
运行结果是:
ABCDEF
BCDEF
CDEF
DEF
EF
F
更多的技术细节可以看一下相关文档:
https://en.cppreference.com/w/cpp/container/span
正如string_view,其实这个span也可以当做一个连续内存的view,这样理解起来就更容易,不过从实际的角度来看又有所不同。还是需要自己认真的去把相关的细节和实现仔细的翻看一下,c++的库越来越大,掌握起来也愈发的要花费更多 的精神。在早先的微软的GSL版本库中,就提供过这个模板类,其它的库也有过类似的操作,c++标准库只是吸收了这些有用的提案并重新融合。从这一点看,海纳百川,还是要谦虚谨慎,睁开眼睛看世界,不要自以为是。
做人和做技术,本质没有不同。