多维数组
这个特性用于访问多维数组,之前C++ operator[] 只支持访问单个下标,无法访问多维数组。
因此要访问多维数组,以前的方式是:
- 重载operator(),于是能够以m(1, 2) 来访问第1 行第2 个元素。但这种方式容易和函数调
用产生混淆; - 重载operator[],并以std::initializer_list 作为参数,然后便能以m[{1, 2}] 来访问元
素。但这种方式看着别扭; - 链式链接operator[],然后就能够以m[1][2] 来访问元素。C语言二维数组的访问方式;
- 定义一个at() 成员,然后通过at(1, 2) 访问元素。同样不方便。
在C++23,我们终于可以通过m[1, 2] 这种方式来访问多维数组。一个例子
template <class T, size_t R, size_t C>
struct matrix
{T& operator[](const size_t r, const size_t c) noexcept {return data_[r * C + c];}const T& operator[](const size_t r, const size_t c) const noexcept {return data_[r * C + c];}private:
std::array<T, R * C> data_;
};int main()
{matrix<int, 2, 2> m;m[0, 0] = 0;m[0, 1] = 1;m[1, 0] = 2;m[1, 1] = 3;for (auto i = 0; i < 2; ++i) {for (auto j = 0; j < 2; ++j) {std::cout << m[i, j] << ' ';}std::cout << std::endl;}
}
std::mdspan
std::mdspan
(多维数组视图)是 C++23 新增的非拥有多维数组的视图,用于表示连续对象序列,允许灵活操作多维数据,支持动态维度。 这个连续对象序列可以是一个简单的 C 数组、带有大小的指针、std::array
、std::vector
或 std::string
。mdspan
是一种轻量级的多维数组视图,不持有数据,而是提供了对现有数据的多维访问方式。它结合了指针和多维索引的优点,使得数据访问更加高效和灵活。
在标头 | ||
template< class T, |
模板形参
T | - | 元素类型;既不是抽象类也不是数组类型的完整对象类型。 |
Extents | - | 指定维数及各维大小,均为编译时已知。必须是 std::extents 的特化。 |
LayoutPolicy | - | 指定如何将多维索引转换为底层的一维索引(列优先三维数组、对称三角二维矩阵等)。必须满足布局映射策略 (LayoutMappingPolicy) 。 |
AccessorPolicy | - | 指定如何将底层一维索引转换为对 T 的引用。必须满足 std::is_same_v<T, typename AccessorPolicy::element_type> 为 true 的约束条件。必须满足访问器策略 (AccessorPolicy) |
由于 C++17 中的类模板参数推导(CTAD),编译器通常可以自动从初始化器的类型推导出模板参数。
成员函数
(构造函数) | 构造一个 mdspan (公开成员函数) |
operator= | 给一个 mdspan 赋值(公开成员函数) |
元素访问 | |
operator[] | 访问指定多维索引处的元素 (公开成员函数) |
观察器 | |
size | 返回多维索引空间的大小 (公开成员函数) |
empty | 检查索引空间大小是否为零 (公开成员函数) |
stride | 获取沿指定维度的步长 (公开成员函数) |
extents | 获取范围(extent)对象 (公开成员函数) |
data_handle | 获取指向底层一维序列的指针 (公开成员函数) |
mapping | 获取映射(mapping)对象 (公开成员函数) |
accessor | 获取访问器策略对象 (公开成员函数) |
is_unique | 确定此 mdspan 的映射是否唯一(每个索引组合映射到不同的基础元素) (公开成员函数) |
is_exhaustive | 确定此 mdspan 的映射是否详尽(exhaustive)(可以使用某些索引组合访问每个底层元素) (公开成员函数) |
is_strided | 确定此 mdspan 的映射是否跨步(在每个维度中,每次递增索引都会跳过相同数量的基础元素) (公开成员函数) |
is_always_unique [静态] | 确定此 mdspan 的布局映射是否始终唯一(unique) (公开静态成员函数) |
is_always_exhaustive [静态] | 确定此 mdspan 的布局映射(layout mapping)是否总是详尽的 (公开静态成员函数) |
is_always_strided [静态] | 确定此 mdspan 的布局映射是否始终跨步(strided) (公开静态成员函数) |
非成员函数
std::swap(std::mdspan) (C++23) | 针对 mdspan 特化的 std::swap 算法 (函数模板) |
子视图 | |
submdspan (C++26) | 返回现存 mdspan 的子集上的视图(函数模板) |
submdspan_extents (C++26) | 从现存 extents 和分片说明符创建新的 extents (函数模板) |
辅助类型和模板
extents (C++23) | 某秩多维索引空间的一个描述符 (类模板) |
dextentsdims (C++23)(C++26) | 全动态 std::extents 的方便别名模板 (别名模板) |
default_accessor (C++23) | 指示索引访问 mdspan 元素的方式的类型(类模板) |
aligned_accessor (C++26) | 提供按对齐访问 mdspan 成员的类型(类模板) |
布局映射策略 | |
layout_left (C++23) | 列优先多维数组布局映射策略;最左边的尺度具有步幅 1 (类) |
layout_right (C++23) | 行优先多维数组布局映射策略;最右边的尺度具有步幅 1 (类) |
layout_stride (C++23) | 具有用户自定义步长的布局映射策略 (类) |
layout_left_padded (C++26) | 具有可大于或等于最左侧尺度的填充跨步的列主序布局映射策略 (类) |
layout_right_padded (C++26) | 具有可大于或等于最右侧尺度的填充跨步的行主序布局映射策略 (类) |
子视图辅助项 | |
full_extentfull_extent_t (C++26) | 切片说明符标签,描述指定尺度的全部索引范围。 (标签) |
strided_slice (C++26) | 切片说明符,表示一组按照偏移量、尺度和跨步三值指定的有规律分布的索引 (类模板) |
submdspan_mapping_result (C++26) | 各 submdspan_mapping 重载的返回类型(类模板) |
示例 1:基本使用
#include<iostream>
#include <mdspan>int main() {// 一维连续存储(模拟二维数组)int data[] = {1,2,3,4,5,6,7,8,9,10,11,12};// 创建 3x4 的二维视图std::mdspan mat(data, 3, 4); // 访问元素 [行][列]std::cout << mat[1][2]; // 输出 7// 修改元素mat[2][3] = 42; // 修改第3行第4列的元素
}
示例 2:动态维度
#include <mdspan>
#include <vector>int main() {std::vector<int> data(20); // 动态存储// 创建 4x5 的动态视图std::mdspan<int, std::dextents<2>> dyn_span(data.data(), 4, 5);dyn_span[3][4] = 100; // 最后一元素
}
3. 多维数组的布局策略
mdspan
允许指定内存布局,优化访问模式:
-
行优先(C风格):
std::layout_right
(默认) -
列优先(Fortran风格):
std::layout_left
#include <mdspan>int main() {int data[12] = { /* ... */ };// 列优先的 3x4 视图std::mdspan<int, std::extents<3,4>, std::layout_left> col_major(data);
}
4. 切片与子视图
mdspan
支持通过切片操作获取子视图:
auto sub_view = std::submdspan(mat, 1, std::full_extent); // 获取第2行所有列
std::cout << sub_view[2]; // 输出原数组的 mat[1][2]
5. 结合算法使用
多维数组与标准库算法协同工作:
#include <algorithm>
#include <mdspan>int main()
{int data[3][4] = { /* ... */ };std::mdspan mat(data);// 遍历所有元素std::for_each(mat.data_handle(), mat.data_handle() + mat.size(), [](int& x) { x *= 2; });
}
5. 布局策略
std::mdspan
允许您指定用于访问底层内存的布局策略。默认情况下,使用 std::layout_right
(C、C++ 或 Python 风格),但您也可以指定 std::layout_left
(Fortran 或 MATLAB 风格)。
使用布局策略std::mdspan
和std::layout_right
遍历两个std::layout_left
可以看出差异。
#include <mdspan>
#include <iostream>
#include <vector>int main()
{std::vector myVec{1, 2, 3, 4, 5, 6, 7, 8};std::mdspan<int, std::extents<std::size_t, // (1)std::dynamic_extent, std::dynamic_extent>, std::layout_right> m{myVec.data(), 4, 2};std::cout << "m.rank(): " << m.rank() << '\n';for (std::size_t i = 0; i < m.extent(0); ++i) {for (std::size_t j = 0; j < m.extent(1); ++j) {std::cout << m[i, j] << ' '; }std::cout << '\n';}std::cout << '\n';std::mdspan<int, std::extents<std::size_t, // (2)std::dynamic_extent, std::dynamic_extent>, std::layout_left> m2{myVec.data(), 4, 2};std::cout << "m2.rank(): " << m2.rank() << '\n';for (std::size_t i = 0; i < m2.extent(0); ++i) {for (std::size_t j = 0; j < m2.extent(1); ++j) {std::cout << m2[i, j] << ' '; }std::cout << '\n';}}
6. 性能与注意事项
-
连续存储:
mdspan
不管理内存,需确保底层数据连续。 -
边界检查:默认无越界检查,可通过自定义策略添加。
-
灵活性:适用于科学计算、图像处理等需要多维数据的场景。
总结
-
传统多维数组:适合静态、编译时已知维度的场景。
-
std::mdspan
:提供动态维度、灵活布局和高效访问,是 C++23 处理多维数据的现代方式。