Add helpers to array that return the size and strides as a std::span (#5974)
* Add helper functions to pybind11::array to return the shape and strides as a std::span. These functions are hidden with macros unless PYBIND11_CPP20 is defined and the <span> include has been found.
* style: pre-commit fixes
* tests: Add unit tests for shape_span() and strides_span()
Add comprehensive unit tests for the new std::span helper functions:
- Test 0D, 1D, 2D, and 3D arrays
- Verify spans match regular shape()/strides() methods
- Test that spans can be used to construct new arrays
- Tests are conditionally compiled only when PYBIND11_HAS_SPAN is defined
* Use __cpp_lib_span feature test macro instead of __has_include
Replace __has_include(<span>) check with __cpp_lib_span feature test macro
to resolve ambiguity where some pre-C++20 systems might have a global
header called <span> that isn't the C++20 std::span.
The check is moved after <version> is included, consistent with how
__cpp_lib_char8_t is handled.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix: Use py::ssize_t instead of ssize_t in span tests
On Windows/MSVC, ssize_t is not available in the standard namespace
without proper includes. Use py::ssize_t (the pybind11 typedef) instead
to ensure cross-platform compatibility.
Fixes compilation errors on:
- Windows/MSVC 2022 (C++20)
- GCC 10 (C++20)
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h
index 19ebc85..017d134 100644
--- a/include/pybind11/detail/common.h
+++ b/include/pybind11/detail/common.h
@@ -252,6 +252,10 @@
# define PYBIND11_HAS_U8STRING 1
#endif
+#if defined(PYBIND11_CPP20) && defined(__cpp_lib_span) && __cpp_lib_span >= 202002L
+# define PYBIND11_HAS_SPAN 1
+#endif
+
// See description of PR #4246:
#if !defined(PYBIND11_NO_ASSERT_GIL_HELD_INCREF_DECREF) && !defined(NDEBUG) \
&& !defined(PYPY_VERSION) && !defined(PYBIND11_ASSERT_GIL_HELD_INCREF_DECREF)
diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h
index 6fa6c77..408d169 100644
--- a/include/pybind11/numpy.h
+++ b/include/pybind11/numpy.h
@@ -29,6 +29,10 @@
#include <utility>
#include <vector>
+#ifdef PYBIND11_HAS_SPAN
+# include <span>
+#endif
+
#if defined(PYBIND11_NUMPY_1_ONLY)
# error "PYBIND11_NUMPY_1_ONLY is no longer supported (see PR #5595)."
#endif
@@ -1143,6 +1147,13 @@
/// Dimensions of the array
const ssize_t *shape() const { return detail::array_proxy(m_ptr)->dimensions; }
+#ifdef PYBIND11_HAS_SPAN
+ /// Dimensions of the array as a span
+ std::span<const ssize_t, std::dynamic_extent> shape_span() const {
+ return std::span(shape(), static_cast<std::size_t>(ndim()));
+ }
+#endif
+
/// Dimension along a given axis
ssize_t shape(ssize_t dim) const {
if (dim >= ndim()) {
@@ -1154,6 +1165,13 @@
/// Strides of the array
const ssize_t *strides() const { return detail::array_proxy(m_ptr)->strides; }
+#ifdef PYBIND11_HAS_SPAN
+ /// Strides of the array as a span
+ std::span<const ssize_t, std::dynamic_extent> strides_span() const {
+ return std::span(strides(), static_cast<std::size_t>(ndim()));
+ }
+#endif
+
/// Stride along a given axis
ssize_t strides(ssize_t dim) const {
if (dim >= ndim()) {
diff --git a/tests/test_numpy_array.cpp b/tests/test_numpy_array.cpp
index ac6b1cf..3bce988 100644
--- a/tests/test_numpy_array.cpp
+++ b/tests/test_numpy_array.cpp
@@ -14,6 +14,7 @@
#include <cstdint>
#include <utility>
+#include <vector>
// Size / dtype checks.
struct DtypeCheck {
@@ -246,6 +247,22 @@
sm.def("nbytes", [](const arr &a) { return a.nbytes(); });
sm.def("owndata", [](const arr &a) { return a.owndata(); });
+#ifdef PYBIND11_HAS_SPAN
+ // test_shape_strides_span
+ sm.def("shape_span", [](const arr &a) {
+ auto span = a.shape_span();
+ return std::vector<py::ssize_t>(span.begin(), span.end());
+ });
+ sm.def("strides_span", [](const arr &a) {
+ auto span = a.strides_span();
+ return std::vector<py::ssize_t>(span.begin(), span.end());
+ });
+ // Test that spans can be used to construct new arrays
+ sm.def("array_from_spans", [](const arr &a) {
+ return py::array(a.dtype(), a.shape_span(), a.strides_span(), a.data(), a);
+ });
+#endif
+
// test_index_offset
def_index_fn(index_at, const arr &);
def_index_fn(index_at_t, const arr_t &);
diff --git a/tests/test_numpy_array.py b/tests/test_numpy_array.py
index 19c07ca..93477aa 100644
--- a/tests/test_numpy_array.py
+++ b/tests/test_numpy_array.py
@@ -68,6 +68,45 @@
assert not m.owndata(a)
+@pytest.mark.skipif(not hasattr(m, "shape_span"), reason="std::span not available")
+def test_shape_strides_span():
+ # Test 0-dimensional array (scalar)
+ a = np.array(42, "f8")
+ assert m.ndim(a) == 0
+ assert m.shape_span(a) == []
+ assert m.strides_span(a) == []
+
+ # Test 1-dimensional array
+ a = np.array([1, 2, 3, 4], "u2")
+ assert m.ndim(a) == 1
+ assert m.shape_span(a) == [4]
+ assert m.strides_span(a) == [2]
+
+ # Test 2-dimensional array
+ a = np.array([[1, 2, 3], [4, 5, 6]], "u2").view()
+ a.flags.writeable = False
+ assert m.ndim(a) == 2
+ assert m.shape_span(a) == [2, 3]
+ assert m.strides_span(a) == [6, 2]
+
+ # Test 3-dimensional array
+ a = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], "i4")
+ assert m.ndim(a) == 3
+ assert m.shape_span(a) == [2, 2, 2]
+ # Verify spans match regular shape/strides
+ assert list(m.shape_span(a)) == list(m.shape(a))
+ assert list(m.strides_span(a)) == list(m.strides(a))
+
+ # Test that spans can be used to construct new arrays
+ original = np.array([[1, 2, 3], [4, 5, 6]], "f4")
+ new_array = m.array_from_spans(original)
+ assert new_array.shape == original.shape
+ assert new_array.strides == original.strides
+ assert new_array.dtype == original.dtype
+ # Verify data is shared (since we pass the same data pointer)
+ np.testing.assert_array_equal(new_array, original)
+
+
@pytest.mark.parametrize(
("args", "ret"), [([], 0), ([0], 0), ([1], 3), ([0, 1], 1), ([1, 2], 5)]
)