diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac0f7aef..cfffd6ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -194,6 +194,53 @@ jobs: working-directory: ${{github.workspace}}/build/src/examples run: $GITHUB_WORKSPACE/.github/run_examples.sh + # Job testing clang with C++23, libc++ and MDSPAN + # ================================================ + Linux_Clang_CXX23_MDSPAN: + runs-on: ubuntu-24.04 + env: + CC: clang-21 + CXX: clang++-21 + + steps: + - uses: actions/checkout@v5 + with: + submodules: true + + - name: "Add LLVM repository" + run: | + wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/llvm-snapshot.gpg + 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 + + - name: "Install libraries" + run: | + sudo apt-get -qq update + sudo apt-get -qq install clang-21 libc++-21-dev libc++abi-21-dev libhdf5-dev libsz2 ninja-build + + - name: Build + run: | + CMAKE_OPTIONS=( + -GNinja + -DCMAKE_CXX_STANDARD=23 + -DHIGHFIVE_USE_LIBCXX:BOOL=ON + -DHIGHFIVE_TEST_MDSPAN:BOOL=ON + ) + source $GITHUB_WORKSPACE/.github/build.sh + + - name: Test + working-directory: ${{github.workspace}}/build + run: | + ctest -j2 --output-on-failure -C $BUILD_TYPE + + - name: Test No HDF5 Diagnositics + working-directory: ${{github.workspace}}/build + run: | + ! ctest --verbose -C $BUILD_TYPE | grep HDF5-DIAG + + - name: Examples + working-directory: ${{github.workspace}}/build/src/examples + run: $GITHUB_WORKSPACE/.github/run_examples.sh + # Job running unit-test with sanitizers # ===================================== Linux_Sanitizers: diff --git a/CMakeLists.txt b/CMakeLists.txt index 079979e2..3c6b64cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,17 +58,26 @@ else() set(HIGHFIVE_TEST_SPAN_DEFAULT Off) endif() +if (CMAKE_CXX_STANDARD GREATER_EQUAL 23) + include(CheckIncludeFileCXX) + CHECK_INCLUDE_FILE_CXX(mdspan HIGHFIVE_TEST_MDSPAN_DEFAULT) +else() + set(HIGHFIVE_TEST_MDSPAN_DEFAULT Off) +endif() + option(HIGHFIVE_UNIT_TESTS "Compile unit-tests" ${HIGHFIVE_EXTRAS_DEFAULT}) option(HIGHFIVE_EXAMPLES "Compile examples" ${HIGHFIVE_EXTRAS_DEFAULT}) option(HIGHFIVE_BUILD_DOCS "Build documentation" ${HIGHFIVE_EXTRAS_DEFAULT}) option(HIGHFIVE_TEST_SPAN "Enable testing std::span, requires C++20" ${HIGHFIVE_TEST_SPAN_DEFAULT}) +option(HIGHFIVE_TEST_MDSPAN "Enable testing std::mdspan, requires C++23 and libc++" ${HIGHFIVE_TEST_MDSPAN_DEFAULT}) option(HIGHFIVE_TEST_BOOST "Enable testing Boost features" OFF) option(HIGHFIVE_TEST_BOOST_SPAN "Additionally, enable testing `boost::span`" OFF) option(HIGHFIVE_TEST_EIGEN "Enable testing Eigen" OFF) option(HIGHFIVE_TEST_OPENCV "Enable testing OpenCV" OFF) option(HIGHFIVE_TEST_XTENSOR "Enable testing xtensor" OFF) option(HIGHFIVE_TEST_HALF_FLOAT "Enable testing half-precision floats" OFF) +option(HIGHFIVE_USE_LIBCXX "Use libc++ for tests/examples (clang only)" OFF) set(HIGHFIVE_MAX_ERRORS 0 CACHE STRING "Maximum number of compiler errors.") option(HIGHFIVE_HAS_WERROR "Convert warnings to errors." OFF) @@ -91,6 +100,10 @@ if(CMAKE_CXX_STANDARD EQUAL 98 OR CMAKE_CXX_STANDARD LESS ${HIGHFIVE_CXX_STANDAR message(FATAL_ERROR "HighFive needs to be compiled with at least C++${HIGHFIVE_CXX_STANDARD_DEFAULT}") endif() +if(HIGHFIVE_TEST_MDSPAN AND CMAKE_CXX_STANDARD LESS 23) + message(FATAL_ERROR "HIGHFIVE_TEST_MDSPAN requires C++23 or newer, but CMAKE_CXX_STANDARD is ${CMAKE_CXX_STANDARD}") +endif() + add_compile_definitions(HIGHFIVE_CXX_STD=${CMAKE_CXX_STANDARD}) # HighFive @@ -175,6 +188,9 @@ if(HIGHFIVE_EXAMPLES OR HIGHFIVE_UNIT_TESTS) include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/HighFiveOptionalDependencies.cmake) endif() +# Include libc++ configuration for examples and tests +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/HighFiveLibCXX.cmake) + if(HIGHFIVE_EXAMPLES) add_subdirectory(src/examples) endif() diff --git a/cmake/HighFiveLibCXX.cmake b/cmake/HighFiveLibCXX.cmake new file mode 100644 index 00000000..31fffe12 --- /dev/null +++ b/cmake/HighFiveLibCXX.cmake @@ -0,0 +1,20 @@ +# HighFiveLibCXX.cmake +# Sets up CMAKE_* variables to use libc++ with clang when HIGHFIVE_USE_LIBCXX is enabled + +if(HIGHFIVE_USE_LIBCXX AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") + # Set compiler flags to use libc++ + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") + + # Set linker flags to use libc++ + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -stdlib=libc++") + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -stdlib=libc++") + + + # Add necessary compile definitions for libc++ + add_compile_definitions(_LIBCPP_VERSION) + + message(STATUS "HighFive: Using libc++ for tests (clang only)") +elseif(HIGHFIVE_USE_LIBCXX AND NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang") + message(WARNING "HIGHFIVE_USE_LIBCXX is enabled but compiler is not clang. libc++ is only supported with clang.") +endif() diff --git a/cmake/HighFiveOptionalDependencies.cmake b/cmake/HighFiveOptionalDependencies.cmake index d8e6f0f1..f3c50941 100644 --- a/cmake/HighFiveOptionalDependencies.cmake +++ b/cmake/HighFiveOptionalDependencies.cmake @@ -52,6 +52,13 @@ if(NOT TARGET HighFiveSpanDependency) endif() endif() +if(NOT TARGET HighFiveMdspanDependency) + add_library(HighFiveMdspanDependency INTERFACE) + if(HIGHFIVE_TEST_MDSPAN) + target_compile_definitions(HighFiveMdspanDependency INTERFACE HIGHFIVE_TEST_MDSPAN=1) + endif() +endif() + if(NOT TARGET HighFiveOptionalDependencies) add_library(HighFiveOptionalDependencies INTERFACE) target_link_libraries(HighFiveOptionalDependencies INTERFACE @@ -60,5 +67,6 @@ if(NOT TARGET HighFiveOptionalDependencies) HighFiveXTensorDependency HighFiveOpenCVDependency HighFiveSpanDependency + HighFiveMdspanDependency ) endif() diff --git a/include/highfive/mdspan.hpp b/include/highfive/mdspan.hpp new file mode 100644 index 00000000..b289a6f0 --- /dev/null +++ b/include/highfive/mdspan.hpp @@ -0,0 +1,167 @@ +#pragma once + +#include "bits/H5Inspector_decl.hpp" +#include "H5Exception.hpp" + +#include +#include +#include +#include +#include +#include + +namespace HighFive { +namespace details { + +// Specialization for std::mdspan +template +struct inspector> { + using type = std::mdspan; + using value_type = typename type::element_type; + using base_type = typename inspector::base_type; + using hdf5_type = base_type; + using extents_type = typename type::extents_type; + using accessor_type = typename type::accessor_type; + + static constexpr size_t ndim = type::rank(); + static constexpr size_t min_ndim = ndim + inspector::min_ndim; + static constexpr size_t max_ndim = ndim + inspector::max_ndim; + + static constexpr bool is_trivially_copyable = + std::is_trivially_copyable::value && + inspector::is_trivially_nestable && + (std::is_same_v, accessor_type> +#ifdef __cpp_lib_aligned_accessor + || std::is_same_v, accessor_type> +#endif + ) && + (std::is_same_v || + (std::is_same_v && ndim == 1)); + static constexpr bool is_trivially_nestable = false; + + private: + using index_type = typename extents_type::index_type; + + // Helper to access the first element (at index 0 in all dimensions) + static auto get_first_element(const type& val) { + std::array indices{}; + return val[indices]; + } + + template + static auto data_impl(T& val) -> decltype(inspector::data(*val.data_handle())) { + if (!is_trivially_copyable) { + throw DataSetException("Invalid use of `inspector>::data`."); + } + + if (val.empty()) { + return nullptr; + } + + return inspector::data(*val.data_handle()); + } + + public: + static size_t getRank(const type& val) { + if (val.empty()) { + return min_ndim; + } + return ndim + inspector::getRank(get_first_element(val)); + } + + static std::vector getDimensions(const type& val) { + std::vector sizes; + sizes.reserve(ndim); + for (size_t r = 0; r < ndim; ++r) { + sizes.push_back(val.extent(r)); + } + if (!val.empty()) { + auto s = inspector::getDimensions(get_first_element(val)); + sizes.insert(sizes.end(), s.begin(), s.end()); + } + return sizes; + } + + static void prepare(type& val, const std::vector& dims) { + if (dims.size() < ndim) { + std::ostringstream os; + os << "Impossible to pair DataSet with " << dims.size() + << " dimensions into an mdspan with rank " << ndim << "."; + throw DataSpaceException(os.str()); + } + + // Check that dimensions match + for (size_t r = 0; r < ndim; ++r) { + if (dims[r] != val.extent(r)) { + std::ostringstream os; + os << "Mismatching dimensions for mdspan: expected " << val.extent(r) + << " for dimension " << r << ", but got " << dims[r] << "."; + throw DataSpaceException(os.str()); + } + } + } + + static hdf5_type* data(type& val) { + return data_impl(val); + } + + static const hdf5_type* data(const type& val) { + return data_impl(val); + } + + static void serialize(const type& val, const std::vector& dims, hdf5_type* m) { + auto subdims = std::vector(dims.begin() + ndim, dims.end()); + auto subsize = compute_total_size(subdims); + + std::array indices{}; + auto iterate = [&](auto& self, size_t dim) -> void { + if (dim == ndim) { + // Base case: serialize element + inspector::serialize(val[indices], subdims, m); + m += subsize; + } else { + // Recursive case: iterate over current dimension + const auto n = static_cast(val.extent(dim)); + for (indices[dim] = 0; indices[dim] < n; ++indices[dim]) { + self(self, dim + 1); + } + } + }; + + iterate(iterate, 0); + } + + static void unserialize(const hdf5_type* vec_align, + const std::vector& dims, + type& val) { + if (dims.size() < ndim) { + std::ostringstream os; + os << "Impossible to pair DataSet with " << dims.size() + << " dimensions into an mdspan with rank " << ndim << "."; + throw DataSpaceException(os.str()); + } + + auto subdims = std::vector(dims.begin() + ndim, dims.end()); + auto subsize = compute_total_size(subdims); + + std::array indices{}; + auto iterate = [&](auto& self, size_t dim) -> void { + if (dim == ndim) { + // Base case: unserialize element + inspector::unserialize(vec_align, subdims, val[indices]); + vec_align += subsize; + } else { + // Recursive case: iterate over current dimension + const auto n = static_cast(dims[dim]); + for (indices[dim] = 0; indices[dim] < n; ++indices[dim]) { + self(self, dim + 1); + } + } + }; + + iterate(iterate, 0); + } +}; + +} // namespace details +} // namespace HighFive diff --git a/src/examples/CMakeLists.txt b/src/examples/CMakeLists.txt index d55b10e0..a445b606 100644 --- a/src/examples/CMakeLists.txt +++ b/src/examples/CMakeLists.txt @@ -23,6 +23,10 @@ set(span_examples ${CMAKE_CURRENT_SOURCE_DIR}/read_write_std_span.cpp ) +set(mdspan_examples + ${CMAKE_CURRENT_SOURCE_DIR}/read_write_std_mdspan.cpp +) + set(easy_examples ${CMAKE_CURRENT_SOURCE_DIR}/easy_attribute.cpp ${CMAKE_CURRENT_SOURCE_DIR}/easy_dumpoptions.cpp @@ -85,6 +89,12 @@ if(HIGHFIVE_TEST_SPAN) endforeach() endif() +if(HIGHFIVE_TEST_MDSPAN) + foreach(example_source ${mdspan_examples}) + compile_example(${example_source} HighFiveFlags) + endforeach() +endif() + if(HIGHFIVE_TEST_BOOST) foreach(example_source ${boost_examples}) compile_example(${example_source} HighFiveFlags HighFiveBoostDependency) diff --git a/src/examples/read_write_std_mdspan.cpp b/src/examples/read_write_std_mdspan.cpp new file mode 100644 index 00000000..d0b4ed6e --- /dev/null +++ b/src/examples/read_write_std_mdspan.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (c), 2025, HighFive Developers + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at + * http://www.boost.org/LICENSE_1_0.txt) + * + */ + +// This example demonstrates using `std::mdspan`. An `std::mdspan` is a +// multi-dimensional view over contiguous memory, similar to `std::span` but +// for multi-dimensional arrays. + +#include +#include +#include +#include + +#include + +#include + +int main(void) { + using namespace HighFive; + + std::string file_name = "read_write_mdspan.h5"; + std::string dataset_name = "array"; + + // Assume we have multi-dimensional data stored contiguously, e.g. in a + // vector. + constexpr size_t rows = 3; + constexpr size_t cols = 4; + std::vector values_row_major(rows * cols); + + // Fill the data + for (size_t i = 0; i < rows; ++i) { + for (size_t j = 0; j < cols; ++j) { + values_row_major[i * cols + j] = double(i * cols + j); + } + } + + // Create a 2D mdspan view over the contiguous memory. + auto view_row_major = std::mdspan(values_row_major.data(), std::extents{rows, cols}); + + { + File file(file_name, File::Truncate); + auto dataset = file.createDataSet(dataset_name, view_row_major); + } + + // Let's read from file. + { + File file(file_name, File::ReadOnly); + auto dataset = file.getDataSet(dataset_name); + + // Assume that memory was allocated by some means, e.g.: + auto dims = dataset.getDimensions(); + auto values_col_major = std::vector(dataset.getElementCount()); + + // Create a column-major mdspan view over the preallocated memory. + auto m = std::layout_left::mapping{std::extents{dims[0], dims[1]}}; + auto view_col_major = std::mdspan(values_col_major.data(), m); + + // ... now we can read into the preallocated memory: + dataset.read(view_col_major); + + // Check that the data was read correctly. + for (size_t i = 0; i < rows; ++i) { + for (size_t j = 0; j < cols; ++j) { + if (view_col_major[i, j] != view_row_major[i, j]) { + std::cerr << "Error: data was not read correctly." << std::endl; + return 1; + } + } + } + } + + return 0; +} diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 458ee8ab..60e9a464 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -75,6 +75,10 @@ foreach(PUBLIC_HEADER ${public_headers}) continue() endif() + if(PUBLIC_HEADER STREQUAL "highfive/mdspan.hpp" AND NOT HIGHFIVE_TEST_MDSPAN) + continue() + endif() + get_filename_component(CLASS_NAME ${PUBLIC_HEADER} NAME_WE) configure_file(tests_import_public_headers.cpp "tests_${CLASS_NAME}.cpp" @ONLY) add_executable("tests_include_${CLASS_NAME}" "${CMAKE_CURRENT_BINARY_DIR}/tests_${CLASS_NAME}.cpp") diff --git a/tests/unit/data_generator.hpp b/tests/unit/data_generator.hpp index 6da177b2..af87b3fb 100644 --- a/tests/unit/data_generator.hpp +++ b/tests/unit/data_generator.hpp @@ -4,8 +4,12 @@ #include #include #include +#include #include #include +#include +#include +#include #include @@ -29,6 +33,10 @@ #include #endif +#ifdef HIGHFIVE_TEST_MDSPAN +#include +#endif + namespace HighFive { namespace testing { @@ -601,6 +609,185 @@ struct ContainerTraits> }; +#endif + +// -- mdspan ------------------------------------------------------------------ +#ifdef HIGHFIVE_TEST_MDSPAN + +template +constexpr const size_t accessor_alignment = alignof(typename Acc::element_type); + +#ifdef __cpp_lib_aligned_accessor +template +constexpr const size_t accessor_alignment> = Align; +#endif + +template +struct ContainerTraits> { + using container_type = std::mdspan; + using value_type = typename container_type::element_type; + using base_type = typename ContainerTraits::base_type; + using extents_type = typename container_type::extents_type; + using layout_type = typename container_type::layout_type; + using accessor_type = typename container_type::accessor_type; + using index_type = typename extents_type::index_type; + + static constexpr bool is_view = true; + static constexpr size_t rank = container_type::rank(); + static constexpr bool is_layout_stride = std::is_same_v; + + static void set(container_type& array, + const std::vector& indices, + const base_type& value) { + auto local_indices = extract_local_indices(indices); + return ContainerTraits::set(array[local_indices], lstrip(indices, rank), value); + } + + static base_type get(const container_type& array, const std::vector& indices) { + auto local_indices = extract_local_indices(indices); + return ContainerTraits::get(array[local_indices], lstrip(indices, rank)); + } + + static void assign(container_type& dst, const container_type& src) { + dst = src; + } + + static std::array make_strides(const std::vector& dims) { + std::array strides{}; + if constexpr (rank > 0) { + // Start with padded left-most stride to ensure non-contiguous layout + strides[0] = 2; // 1 + padding + for (size_t i = 1; i < rank; ++i) { + strides[i] = strides[i - 1] * static_cast(dims[i - 1]); + } + } + return strides; + } + + static size_t container_size(const std::vector& local_dims) { + if constexpr (is_layout_stride && rank > 0) { + auto strides = make_strides(local_dims); + // Calculate max offset: sum of (extent[i]-1) * stride[i] for all i + size_t max_offset = 0; + for (size_t i = 0; i < rank; ++i) { + max_offset += (local_dims[i] - 1) * static_cast(strides[i]); + } + return max_offset + 1; + } else { + return flat_size(local_dims); + } + } + + static container_type make_container(value_type* ptr, const std::vector& local_dims) { + extents_type extents = make_extents(local_dims); + if constexpr (is_layout_stride) { + auto strides = make_strides(local_dims); + typename layout_type::mapping mapping(extents, strides); + return container_type(ptr, mapping); + } else { + return container_type(ptr, extents); + } + } + + static container_type allocate(const std::vector& dims) { + auto local_dims = std::vector(dims.begin(), dims.begin() + rank); + size_t n_elements = container_size(local_dims); + + value_type* ptr = allocate_aligned_memory(n_elements); + container_type array = make_container(ptr, local_dims); + + for (size_t i = 0; i < flat_size(local_dims); ++i) { + auto element = ContainerTraits::allocate(lstrip(dims, rank)); + set(array, unravel(i, local_dims), element); + } + + return array; + } + + static void deallocate(container_type& array, const std::vector& dims) { + auto local_dims = std::vector(dims.begin(), dims.begin() + rank); + size_t n_elements = flat_size(local_dims); + for (size_t i = 0; i < n_elements; ++i) { + auto indices = unravel(i, local_dims); + auto local_indices = extract_local_indices(indices); + ContainerTraits::deallocate(array[local_indices], lstrip(dims, rank)); + } + + free_aligned_memory(array.data_handle()); + } + + static void sanitize_dims(std::vector& dims, size_t axis) { + // Check each extent and update dims if it's static + for (size_t r = 0; r < rank; ++r) { + if (extents_type::static_extent(r) != std::dynamic_extent) { + dims[axis + r] = static_cast(extents_type::static_extent(r)); + } + } + ContainerTraits::sanitize_dims(dims, axis + rank); + } + + private: + static value_type* allocate_aligned_memory(size_t n_elements) { + if (n_elements == 0) { + return nullptr; + } + + // Memory layout: + // raw_ptr + // │ + // v + // ┌─────────┬──────────│──────────┬──────────┬──────────┐ + // │ padding │ raw_ptr │ data │ data │ ... │ + // └─────────┴──────────│──────────┴──────────┴──────────┘ + // ^ ^ + // │ │ + // stored_ptr ─┘ └── aligned_ptr + + size_t size = n_elements * sizeof(value_type); + size_t alignment = accessor_alignment; + + // Overallocate to ensure we can find an aligned address + size_t total_size = size + alignment - 1 + sizeof(void*); + char* raw_ptr = new char[total_size]; + + // Find the aligned address within the allocation + uintptr_t raw_addr = reinterpret_cast(raw_ptr); + uintptr_t aligned_addr = (raw_addr + sizeof(void*) + alignment - 1) & ~(alignment - 1); + char* aligned_char_ptr = reinterpret_cast(aligned_addr); + + // Store the original pointer just before the aligned address + void** stored_ptr = reinterpret_cast(aligned_char_ptr - sizeof(void*)); + *stored_ptr = raw_ptr; + + return reinterpret_cast(aligned_char_ptr); + } + + static void free_aligned_memory(value_type* aligned_ptr) { + if (aligned_ptr != nullptr) { + return; + } + char* aligned_char_ptr = reinterpret_cast(aligned_ptr); + void** stored_ptr = reinterpret_cast(aligned_char_ptr - sizeof(void*)); + char* original_ptr = static_cast(*stored_ptr); + delete[] original_ptr; + } + + static std::array extract_local_indices(const std::vector& indices) { + std::array local_indices; + for (size_t i = 0; i < rank; ++i) { + local_indices[i] = static_cast(indices[i]); + } + return local_indices; + } + + static extents_type make_extents(const std::vector& dims) { + auto impl = [&dims](std::index_sequence) { + return extents_type{dims[Is]...}; + }; + return impl(std::make_index_sequence{}); + } +}; + #endif // -- XTensor ----------------------------------------------------------------- diff --git a/tests/unit/supported_types.hpp b/tests/unit/supported_types.hpp index ccda4a77..eaadc2a0 100644 --- a/tests/unit/supported_types.hpp +++ b/tests/unit/supported_types.hpp @@ -18,6 +18,10 @@ #include #endif +#ifdef HIGHFIVE_TEST_MDSPAN +#include +#endif + namespace HighFive { namespace testing { @@ -108,6 +112,25 @@ struct XArray { }; #endif +#ifdef HIGHFIVE_TEST_MDSPAN +template +struct STDMdspan { + template + using type = std::mdspan, Extents, Layout>; +}; + +#ifdef __cpp_lib_aligned_accessor +template +struct STDMdspanAligned { + template + using type = std::mdspan, + Extents, + Layout, + std::aligned_accessor, Alignment>>; +}; +#endif +#endif + template struct ContainerProduct; @@ -154,6 +177,7 @@ using some_scalar_types = typename ConcatenateTuples>, scalar_types_eigen>::type, typename ContainerProduct>, scalar_types_eigen>::type, typename ContainerProduct, scalar_types_eigen>::type, +#endif +#ifdef HIGHFIVE_TEST_MDSPAN + typename ContainerProduct, std::layout_left>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_left>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_left>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_left>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_left>>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_left>>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_right>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_right>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_right>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_right>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_right>>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_right>>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_stride>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_stride>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_stride>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_stride>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_stride>>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_stride>>, scalar_types_mdspan>::type, +#ifdef __cpp_lib_aligned_accessor + typename ContainerProduct, std::layout_left>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_left>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_left>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_left>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_left>>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_left>>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_right>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_right>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_right>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_right>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_right>>, scalar_types_mdspan>::type, + typename ContainerProduct, std::layout_right>>, scalar_types_mdspan>::type, +#endif #endif typename ContainerProduct, all_scalar_types>::type, typename ContainerProduct>, some_scalar_types>::type,