在不传递引用的情况下,传递对象在很多时候会巨大的性能损耗。
而入错传递的是一个视图,这个视图能够指向原对象,那么这个直接传递的开销也是我们可以接受的。
在C++20中,std::span
是一种能够指代连续序列的数据结构。
std::span - cppreference.com
具体效果如下:
#include
#include
#include
struct Node {
int x = 0;
Node(int x) : x(x) {
printf("[%s]=>[%p]=>[%d]\n", __func__, this, x);
}
Node(const Node& lhs) {
x = lhs.x;
printf("[%s]=>[%p=>%p]=>[%d]\n", __func__, &lhs, this, x);
}
operator int() {
return x;
}
};
void show_by_vec(std::vector<Node> arr) {
std::cout << __func__ << std::endl;
for (auto&& x : arr) {
std::cout << x << ' ';
}
std::cout << std::endl;
}
void show_by_span(std::span<Node> arr) {
std::cout << __func__ << std::endl;
for (auto&& x : arr) {
std::cout << x << ' ';
}
std::cout << std::endl;
}
int main() {
std::vector<Node> arr = {1, 2, 3};
std::cout << "++++++++++++++++++++++++++++" << std::endl;
show_by_vec(arr);
show_by_span(arr);
}
输出如下:
从输出可以看出,span相比较于直接传递对象不会有过多的开销
[Node]=>[000001368f2513e0]=>[1]
[Node]=>[000001368f2513e4]=>[2]
[Node]=>[000001368f2513e8]=>[3]
++++++++++++++++++++++++++++
[Node]=>[000001368f2513e0=>000001368f251400]=>[1]
[Node]=>[000001368f2513e4=>000001368f251404]=>[2]
[Node]=>[000001368f2513e8=>000001368f251408]=>[3]
show_by_vec
1 2 3
show_by_span
1 2 3
常见的连续序列大概有以下几种
注意:
#include
#include
#include
#include
#include
namespace name = std;
void show(name::span<int> arr) {
for (auto&& elem : arr) {
std::cout << elem << ' ';
}
std::cout << std::endl;
}
int main() {
// 传统的C式数组
int c_arr[] = {1, 2, 3, 4};
show(c_arr); // 直接传数组
show({c_arr, std::size(c_arr)}); // 传递指针+长度
// std::array
std::array arr = {11, 22, 33, 44};
show(arr); // 直接传std::array
// std::vector
std::vector vec{111, 222, 333, 444};
show(vec); // 直接传vector
show({vec.begin(), vec.end()}); // 首尾迭代器
show({vec.begin(), vec.size()}); // 首迭代器+长度
// 链表不是连续序列
// std::list lst = {1111, 2222, 3333, 4444};
// show(list);
// std::deque 也不是
}
注意本文只是一个简单实现版本
没有考虑各种特殊情况,也没有用 SFINAE(Substitution Failure Is Not An Error)
来进行约束。
一些简单的约束方法可以参考 > (C++) 一个例子,了解 SFINAE 从 cpp11 到 cpp20 的核心技巧 - 知乎 (zhihu.com)
直接上代码:
#include
#include // cpp20 std::to_address
#include
namespace my {
// 只考虑一个模板参数的情况
template <typename Type>
class span final {
private:
Type* ptr = nullptr;
size_t size = 0;
public:
constexpr span() noexcept = default;
constexpr span(const span&) noexcept = default;
constexpr span& operator=(const span&) noexcept = default;
constexpr ~span() noexcept = default;
public: // 针对具体类型
// 数组是无法传递对象,只能传递引用
template <size_t N>
constexpr span(Type (&arr)[N]) noexcept {
this->ptr = arr;
this->size = N;
}
// 针对std::array
template <size_t N>
constexpr span(std::array<Type, N>& arr) noexcept {
this->ptr = arr.data();
this->size = N;
}
// 针对std::vector
constexpr span(std::vector<Type>& arr) noexcept {
this->ptr = arr.data();
this->size = arr.size();
}
public: // 针对迭代式 使用cpp20的`std::to_address`可以同时应对迭代器和指针
// 首位置和长度
template <typename Iter>
constexpr span(Iter iter, size_t length) noexcept {
this->ptr = std::to_address(iter);
this->size = length;
}
// 首位置和尾位置
template <typename Iter>
constexpr span(Iter first, Iter end) noexcept {
this->ptr = std::to_address(first);
this->size = end - first;
}
public:
constexpr Type* begin() const noexcept {
return this->ptr;
}
constexpr Type* end() const noexcept {
return this->ptr + this->size;
}
};
} // namespace my
测试代码与上文一致:
提示:将该模块的两份代码直接顺序复制到一个文档中即可
#include
namespace name = my;
void show(name::span<int> arr) {
for (auto&& elem : arr) {
std::cout << elem << ' ';
}
std::cout << std::endl;
}
int main() {
// 传统的C式数组
int c_arr[] = {1, 2, 3, 4};
show(c_arr); // 直接传数组
show({c_arr, std::size(c_arr)}); // 传递指针+长度
// std::array
std::array arr = {11, 22, 33, 44};
show(arr); // 直接传std::array
// std::vector
std::vector vec{111, 222, 333, 444};
show(vec); // 直接传vector
show({vec.begin(), vec.end()}); // 首尾迭代器
show({vec.begin(), vec.size()}); // 首迭代器+长度
}
这里的主要难点就是对构造函数的实现,如何获得首地址和长度。
大致可以分为两类
关于std::vector和std::array相对比较方便,也是大家平时直接应用的方式。
而关于数组,很多人并不熟悉怎么操作。
注意Type (&arr)[N]
的写法这里必须要套小括号,因为[]
的优先级比&
高,不然无法获得数组的引用。
std::array和普通数组一样也是使用template
。可见具有动态长度的std::vector在这里是最方便的。
public: // 针对具体类型
// 数组是无法传递对象,只能传递引用
template <size_t N>
constexpr span(Type (&arr)[N]) noexcept {
this->ptr = arr;
this->size = N;
}
// 针对std::array
template <size_t N>
constexpr span(std::array<Type, N>& arr) noexcept {
this->ptr = arr.data();
this->size = N;
}
// 针对std::vector
constexpr span(std::vector<Type>& arr) noexcept {
this->ptr = arr.data();
this->size = arr.size();
}
首地址+长度
是C语言中传数组的最常见形式。
到了C++中泛化出了迭代器的标准。为了同时适配普通指针和迭代器,在C++20中推出了std::to_address()
的方法。可以多态的转化到指针中。这样就不用针对指针和迭代器写两个版本了。
关于这里的第二个传入首位位置的版本,需要允许两者直接的相互减。
请注意各种迭代器的区别:迭代器库 - cppreference.com
public: // 针对迭代式 使用cpp20的`std::to_address`可以同时应对迭代器和指针
// 首位置和长度
template <typename Iter>
constexpr span(Iter iter, size_t length) noexcept {
this->ptr = std::to_address(iter);
this->size = length;
}
// 首位置和尾位置
template <typename Iter>
constexpr span(Iter first, Iter end) noexcept {
this->ptr = std::to_address(first);
this->size = end - first;
}