Skip to content

Commit 1d916be

Browse files
committed
add: support for std::mdspan.
Supports layout_{left,right,stride} and both accessors, i.e. default_accessor and aligned_accessor. The padded layouts should also work, but since they're not implemented in any released version of (any of) the standard library, we're not testing them (yet). Copies are avoided for row-major layouts (layout_right) and for one dimensional column-major layouts.
1 parent 7f46d00 commit 1d916be

File tree

9 files changed

+537
-0
lines changed

9 files changed

+537
-0
lines changed

.github/workflows/ci.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,53 @@ jobs:
194194
working-directory: ${{github.workspace}}/build/src/examples
195195
run: $GITHUB_WORKSPACE/.github/run_examples.sh
196196

197+
# Job testing clang with C++23, libc++ and MDSPAN
198+
# ================================================
199+
Linux_Clang_CXX23_MDSPAN:
200+
runs-on: ubuntu-24.04
201+
env:
202+
CC: clang-21
203+
CXX: clang++-21
204+
205+
steps:
206+
- uses: actions/checkout@v5
207+
with:
208+
submodules: true
209+
210+
- name: "Add LLVM repository"
211+
run: |
212+
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/llvm-snapshot.gpg
213+
echo "deb [signed-by=/usr/share/keyrings/llvm-snapshot.gpg] http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" | sudo tee /etc/apt/sources.list.d/llvm.list
214+
215+
- name: "Install libraries"
216+
run: |
217+
sudo apt-get -qq update
218+
sudo apt-get -qq install clang-21 libc++-21-dev libc++abi-21-dev libhdf5-dev libsz2 ninja-build
219+
220+
- name: Build
221+
run: |
222+
CMAKE_OPTIONS=(
223+
-GNinja
224+
-DCMAKE_CXX_STANDARD=23
225+
-DHIGHFIVE_USE_LIBCXX:BOOL=ON
226+
-DHIGHFIVE_TEST_MDSPAN:BOOL=ON
227+
)
228+
source $GITHUB_WORKSPACE/.github/build.sh
229+
230+
- name: Test
231+
working-directory: ${{github.workspace}}/build
232+
run: |
233+
ctest -j2 --output-on-failure -C $BUILD_TYPE
234+
235+
- name: Test No HDF5 Diagnositics
236+
working-directory: ${{github.workspace}}/build
237+
run: |
238+
! ctest --verbose -C $BUILD_TYPE | grep HDF5-DIAG
239+
240+
- name: Examples
241+
working-directory: ${{github.workspace}}/build/src/examples
242+
run: $GITHUB_WORKSPACE/.github/run_examples.sh
243+
197244
# Job running unit-test with sanitizers
198245
# =====================================
199246
Linux_Sanitizers:

CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,19 @@ else()
5858
set(HIGHFIVE_TEST_SPAN_DEFAULT Off)
5959
endif()
6060

61+
if (CMAKE_CXX_STANDARD GREATER_EQUAL 23)
62+
include(CheckIncludeFileCXX)
63+
CHECK_INCLUDE_FILE_CXX(mdspan HIGHFIVE_TEST_MDSPAN_DEFAULT)
64+
else()
65+
set(HIGHFIVE_TEST_MDSPAN_DEFAULT Off)
66+
endif()
67+
6168
option(HIGHFIVE_UNIT_TESTS "Compile unit-tests" ${HIGHFIVE_EXTRAS_DEFAULT})
6269
option(HIGHFIVE_EXAMPLES "Compile examples" ${HIGHFIVE_EXTRAS_DEFAULT})
6370
option(HIGHFIVE_BUILD_DOCS "Build documentation" ${HIGHFIVE_EXTRAS_DEFAULT})
6471

6572
option(HIGHFIVE_TEST_SPAN "Enable testing std::span, requires C++20" ${HIGHFIVE_TEST_SPAN_DEFAULT})
73+
option(HIGHFIVE_TEST_MDSPAN "Enable testing std::mdspan, requires C++23 and libc++" ${HIGHFIVE_TEST_MDSPAN_DEFAULT})
6674
option(HIGHFIVE_TEST_BOOST "Enable testing Boost features" OFF)
6775
option(HIGHFIVE_TEST_BOOST_SPAN "Additionally, enable testing `boost::span`" OFF)
6876
option(HIGHFIVE_TEST_EIGEN "Enable testing Eigen" OFF)
@@ -92,6 +100,10 @@ if(CMAKE_CXX_STANDARD EQUAL 98 OR CMAKE_CXX_STANDARD LESS ${HIGHFIVE_CXX_STANDAR
92100
message(FATAL_ERROR "HighFive needs to be compiled with at least C++${HIGHFIVE_CXX_STANDARD_DEFAULT}")
93101
endif()
94102

103+
if(HIGHFIVE_TEST_MDSPAN AND CMAKE_CXX_STANDARD LESS 23)
104+
message(FATAL_ERROR "HIGHFIVE_TEST_MDSPAN requires C++23 or newer, but CMAKE_CXX_STANDARD is ${CMAKE_CXX_STANDARD}")
105+
endif()
106+
95107
add_compile_definitions(HIGHFIVE_CXX_STD=${CMAKE_CXX_STANDARD})
96108

97109
# HighFive

cmake/HighFiveOptionalDependencies.cmake

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ if(NOT TARGET HighFiveSpanDependency)
5252
endif()
5353
endif()
5454

55+
if(NOT TARGET HighFiveMdspanDependency)
56+
add_library(HighFiveMdspanDependency INTERFACE)
57+
if(HIGHFIVE_TEST_MDSPAN)
58+
target_compile_definitions(HighFiveMdspanDependency INTERFACE HIGHFIVE_TEST_MDSPAN=1)
59+
endif()
60+
endif()
61+
5562
if(NOT TARGET HighFiveOptionalDependencies)
5663
add_library(HighFiveOptionalDependencies INTERFACE)
5764
target_link_libraries(HighFiveOptionalDependencies INTERFACE
@@ -60,5 +67,6 @@ if(NOT TARGET HighFiveOptionalDependencies)
6067
HighFiveXTensorDependency
6168
HighFiveOpenCVDependency
6269
HighFiveSpanDependency
70+
HighFiveMdspanDependency
6371
)
6472
endif()

include/highfive/mdspan.hpp

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#pragma once
2+
3+
#include "bits/H5Inspector_decl.hpp"
4+
#include "H5Exception.hpp"
5+
6+
#include <mdspan>
7+
#include <vector>
8+
#include <array>
9+
#include <sstream>
10+
#include <utility>
11+
#include <type_traits>
12+
13+
namespace HighFive {
14+
namespace details {
15+
16+
// Specialization for std::mdspan
17+
template <class ElementType, class Extents, class LayoutPolicy, class AccessorPolicy>
18+
struct inspector<std::mdspan<ElementType, Extents, LayoutPolicy, AccessorPolicy>> {
19+
using type = std::mdspan<ElementType, Extents, LayoutPolicy, AccessorPolicy>;
20+
using value_type = typename type::element_type;
21+
using base_type = typename inspector<value_type>::base_type;
22+
using hdf5_type = base_type;
23+
using extents_type = typename type::extents_type;
24+
using accessor_type = typename type::accessor_type;
25+
26+
static constexpr size_t ndim = type::rank();
27+
static constexpr size_t min_ndim = ndim + inspector<value_type>::min_ndim;
28+
static constexpr size_t max_ndim = ndim + inspector<value_type>::max_ndim;
29+
30+
static constexpr bool is_trivially_copyable =
31+
std::is_trivially_copyable<value_type>::value &&
32+
inspector<value_type>::is_trivially_nestable &&
33+
(std::is_same_v<std::default_accessor<value_type>, accessor_type>
34+
#ifdef __cpp_lib_aligned_accessor
35+
|| std::is_same_v<std::aligned_accessor<value_type>, accessor_type>
36+
#endif
37+
) &&
38+
(std::is_same_v<typename type::layout_type, std::layout_right> ||
39+
(std::is_same_v<typename type::layout_type, std::layout_left> && ndim == 1));
40+
static constexpr bool is_trivially_nestable = false;
41+
42+
private:
43+
using index_type = typename extents_type::index_type;
44+
45+
// Helper to access the first element (at index 0 in all dimensions)
46+
static auto get_first_element(const type& val) {
47+
std::array<index_type, ndim> indices{};
48+
return val[indices];
49+
}
50+
51+
public:
52+
static size_t getRank(const type& val) {
53+
if (val.empty()) {
54+
return min_ndim;
55+
}
56+
return ndim + inspector<value_type>::getRank(get_first_element(val));
57+
}
58+
59+
static std::vector<size_t> getDimensions(const type& val) {
60+
std::vector<size_t> sizes;
61+
sizes.reserve(ndim);
62+
for (size_t r = 0; r < ndim; ++r) {
63+
sizes.push_back(val.extent(r));
64+
}
65+
if (!val.empty()) {
66+
auto s = inspector<value_type>::getDimensions(get_first_element(val));
67+
sizes.insert(sizes.end(), s.begin(), s.end());
68+
}
69+
return sizes;
70+
}
71+
72+
static void prepare(type& val, const std::vector<size_t>& dims) {
73+
if (dims.size() < ndim) {
74+
std::ostringstream os;
75+
os << "Impossible to pair DataSet with " << dims.size()
76+
<< " dimensions into an mdspan with rank " << ndim << ".";
77+
throw DataSpaceException(os.str());
78+
}
79+
80+
// Check that dimensions match
81+
for (size_t r = 0; r < ndim; ++r) {
82+
if (dims[r] != val.extent(r)) {
83+
std::ostringstream os;
84+
os << "Mismatching dimensions for mdspan: expected " << val.extent(r)
85+
<< " for dimension " << r << ", but got " << dims[r] << ".";
86+
throw DataSpaceException(os.str());
87+
}
88+
}
89+
}
90+
91+
static hdf5_type* data(type& val) {
92+
if (!is_trivially_copyable) {
93+
throw DataSetException("Invalid use of `inspector<std::mdspan<...>>::data`.");
94+
}
95+
96+
if (val.empty()) {
97+
return nullptr;
98+
}
99+
100+
return inspector<value_type>::data(*val.data_handle());
101+
}
102+
103+
static const hdf5_type* data(const type& val) {
104+
if (!is_trivially_copyable) {
105+
throw DataSetException("Invalid use of `inspector<std::mdspan<...>>::data`.");
106+
}
107+
108+
if (val.empty()) {
109+
return nullptr;
110+
}
111+
112+
return inspector<value_type>::data(*val.data_handle());
113+
}
114+
115+
static void serialize(const type& val, const std::vector<size_t>& dims, hdf5_type* m) {
116+
auto subdims = std::vector<size_t>(dims.begin() + ndim, dims.end());
117+
auto subsize = compute_total_size(subdims);
118+
119+
std::array<index_type, ndim> indices{};
120+
auto iterate = [&](auto& self, size_t dim) -> void {
121+
if (dim == ndim) {
122+
// Base case: serialize element
123+
inspector<value_type>::serialize(val[indices], subdims, m);
124+
m += subsize;
125+
} else {
126+
// Recursive case: iterate over current dimension
127+
const auto n = static_cast<index_type>(val.extent(dim));
128+
for (indices[dim] = 0; indices[dim] < n; ++indices[dim]) {
129+
self(self, dim + 1);
130+
}
131+
}
132+
};
133+
134+
iterate(iterate, 0);
135+
}
136+
137+
static void unserialize(const hdf5_type* vec_align,
138+
const std::vector<size_t>& dims,
139+
type& val) {
140+
if (dims.size() < ndim) {
141+
std::ostringstream os;
142+
os << "Impossible to pair DataSet with " << dims.size()
143+
<< " dimensions into an mdspan with rank " << ndim << ".";
144+
throw DataSpaceException(os.str());
145+
}
146+
147+
auto subdims = std::vector<size_t>(dims.begin() + ndim, dims.end());
148+
auto subsize = compute_total_size(subdims);
149+
150+
std::array<index_type, ndim> indices{};
151+
auto iterate = [&](auto& self, size_t dim) -> void {
152+
if (dim == ndim) {
153+
// Base case: unserialize element
154+
inspector<value_type>::unserialize(vec_align, subdims, val[indices]);
155+
vec_align += subsize;
156+
} else {
157+
// Recursive case: iterate over current dimension
158+
const auto n = static_cast<index_type>(dims[dim]);
159+
for (indices[dim] = 0; indices[dim] < n; ++indices[dim]) {
160+
self(self, dim + 1);
161+
}
162+
}
163+
};
164+
165+
iterate(iterate, 0);
166+
}
167+
};
168+
169+
} // namespace details
170+
} // namespace HighFive

src/examples/CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ set(span_examples
2323
${CMAKE_CURRENT_SOURCE_DIR}/read_write_std_span.cpp
2424
)
2525

26+
set(mdspan_examples
27+
${CMAKE_CURRENT_SOURCE_DIR}/read_write_std_mdspan.cpp
28+
)
29+
2630
set(easy_examples
2731
${CMAKE_CURRENT_SOURCE_DIR}/easy_attribute.cpp
2832
${CMAKE_CURRENT_SOURCE_DIR}/easy_dumpoptions.cpp
@@ -85,6 +89,12 @@ if(HIGHFIVE_TEST_SPAN)
8589
endforeach()
8690
endif()
8791

92+
if(HIGHFIVE_TEST_MDSPAN)
93+
foreach(example_source ${mdspan_examples})
94+
compile_example(${example_source} HighFiveFlags)
95+
endforeach()
96+
endif()
97+
8898
if(HIGHFIVE_TEST_BOOST)
8999
foreach(example_source ${boost_examples})
90100
compile_example(${example_source} HighFiveFlags HighFiveBoostDependency)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright (c), 2025, HighFive Developers
3+
*
4+
* Distributed under the Boost Software License, Version 1.0.
5+
* (See accompanying file LICENSE_1_0.txt or copy at
6+
* http://www.boost.org/LICENSE_1_0.txt)
7+
*
8+
*/
9+
10+
// This example demonstrates using `std::mdspan`. An `std::mdspan` is a
11+
// multi-dimensional view over contiguous memory, similar to `std::span` but
12+
// for multi-dimensional arrays.
13+
14+
#include <string>
15+
#include <vector>
16+
#include <mdspan>
17+
#include <cassert>
18+
19+
#include <highfive/highfive.hpp>
20+
21+
#include <highfive/mdspan.hpp>
22+
23+
int main(void) {
24+
using namespace HighFive;
25+
26+
std::string file_name = "read_write_mdspan.h5";
27+
std::string dataset_name = "array";
28+
29+
// Assume we have multi-dimensional data stored contiguously, e.g. in a
30+
// vector.
31+
constexpr size_t rows = 3;
32+
constexpr size_t cols = 4;
33+
std::vector<double> values_row_major(rows * cols);
34+
35+
// Fill the data
36+
for (size_t i = 0; i < rows; ++i) {
37+
for (size_t j = 0; j < cols; ++j) {
38+
values_row_major[i * cols + j] = double(i * cols + j);
39+
}
40+
}
41+
42+
// Create a 2D mdspan view over the contiguous memory.
43+
auto view_row_major = std::mdspan(values_row_major.data(), std::extents{rows, cols});
44+
45+
{
46+
File file(file_name, File::Truncate);
47+
auto dataset = file.createDataSet(dataset_name, view_row_major);
48+
}
49+
50+
// Let's read from file.
51+
{
52+
File file(file_name, File::ReadOnly);
53+
auto dataset = file.getDataSet(dataset_name);
54+
55+
// Assume that memory was allocated by some means, e.g.:
56+
auto dims = dataset.getDimensions();
57+
auto values_col_major = std::vector<double>(dataset.getElementCount());
58+
59+
// Create a column-major mdspan view over the preallocated memory.
60+
auto m = std::layout_left::mapping{std::extents{dims[0], dims[1]}};
61+
auto view_col_major = std::mdspan(values_col_major.data(), m);
62+
63+
// ... now we can read into the preallocated memory:
64+
dataset.read(view_col_major);
65+
66+
// Check that the data was read correctly.
67+
for (size_t i = 0; i < rows; ++i) {
68+
for (size_t j = 0; j < cols; ++j) {
69+
if (view_col_major[i, j] != view_row_major[i, j]) {
70+
std::cerr << "Error: data was not read correctly." << std::endl;
71+
return 1;
72+
}
73+
}
74+
}
75+
}
76+
77+
return 0;
78+
}

tests/unit/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ foreach(PUBLIC_HEADER ${public_headers})
7575
continue()
7676
endif()
7777

78+
if(PUBLIC_HEADER STREQUAL "highfive/mdspan.hpp" AND NOT HIGHFIVE_TEST_MDSPAN)
79+
continue()
80+
endif()
81+
7882
get_filename_component(CLASS_NAME ${PUBLIC_HEADER} NAME_WE)
7983
configure_file(tests_import_public_headers.cpp "tests_${CLASS_NAME}.cpp" @ONLY)
8084
add_executable("tests_include_${CLASS_NAME}" "${CMAKE_CURRENT_BINARY_DIR}/tests_${CLASS_NAME}.cpp")

0 commit comments

Comments
 (0)