Skip to content

Commit b5dac61

Browse files
committed
[CPyCppyy] Work around non-instantiable std::span iterators in GCC 15
The libstdc++ (GCC >= 15) implements `std::span::iterator` using a private nested tag type, which makes the iterator non-instantiable by CallFunc-generated wrappers (the return type cannot be named without violating access rules). To preserve correct Python iteration semantics, this commit suggests to replace `begin()`/`end()` for `std::span` to return a custom pointer-based iterator instead. This avoids relying on `std::span::iterator` while still providing a real C++ iterator object that CPyCppyy can also wrap and expose via `__iter__`/`__next__`. A unit test is implemented based on the reproducers provided in #18837. Closes #18837.
1 parent 63d2e22 commit b5dac61

File tree

2 files changed

+148
-0
lines changed

2 files changed

+148
-0
lines changed

bindings/pyroot/cppyy/CPyCppyy/src/Pythonize.cxx

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,111 @@ static ItemGetter* GetGetter(PyObject* args)
314314
return getter;
315315
}
316316

317+
namespace {
318+
319+
void compileSpanHelpers()
320+
{
321+
static bool compiled = false;
322+
323+
if (compiled)
324+
return;
325+
326+
compiled = true;
327+
328+
auto code = R"(
329+
namespace __cppyy_internal {
330+
331+
template <class T>
332+
struct ptr_iterator {
333+
T *cur;
334+
T *end;
335+
336+
ptr_iterator(T *c, T *e) : cur(c), end(e) {}
337+
338+
T &operator*() const { return *cur; }
339+
ptr_iterator &operator++()
340+
{
341+
++cur;
342+
return *this;
343+
}
344+
bool operator==(const ptr_iterator &other) const { return cur == other.cur; }
345+
bool operator!=(const ptr_iterator &other) const { return !(*this == other); }
346+
};
347+
348+
template <class T>
349+
ptr_iterator<T> make_iter(T *begin, T *end)
350+
{
351+
return {begin, end};
352+
}
353+
354+
} // namespace __cppyy_internal
355+
356+
// Note: for const span<T>, T is const-qualified here
357+
template <class T>
358+
auto __cppyy_internal_begin(T &s) noexcept
359+
{
360+
return __cppyy_internal::make_iter(s.data(), s.data() + s.size());
361+
}
362+
363+
// Note: for const span<T>, T is const-qualified here
364+
template <class T>
365+
auto __cppyy_internal_end(T &s) noexcept
366+
{
367+
// end iterator = begin iterator with cur == end
368+
return __cppyy_internal::make_iter(s.data() + s.size(), s.data() + s.size());
369+
}
370+
)";
371+
Cppyy::Compile(code, /*silent*/ true);
372+
}
373+
374+
PyObject *spanBegin()
375+
{
376+
static PyObject *pyFunc = nullptr;
377+
if (!pyFunc) {
378+
compileSpanHelpers();
379+
PyObject *py_ns = CPyCppyy::GetScopeProxy(Cppyy::gGlobalScope);
380+
pyFunc = PyObject_GetAttrString(py_ns, "__cppyy_internal_begin");
381+
if (!pyFunc) {
382+
PyErr_Format(PyExc_RuntimeError, "cppyy internal error: failed to locate helper "
383+
"'__cppyy_internal_begin' for std::span pythonization");
384+
}
385+
}
386+
return pyFunc;
387+
}
388+
389+
PyObject *spanEnd()
390+
{
391+
static PyObject *pyFunc = nullptr;
392+
if (!pyFunc) {
393+
compileSpanHelpers();
394+
PyObject *py_ns = CPyCppyy::GetScopeProxy(Cppyy::gGlobalScope);
395+
pyFunc = PyObject_GetAttrString(py_ns, "__cppyy_internal_end");
396+
if (!pyFunc) {
397+
PyErr_Format(PyExc_RuntimeError, "cppyy internal error: failed to locate helper "
398+
"'__cppyy_internal_end' for std::span pythonization");
399+
}
400+
}
401+
return pyFunc;
402+
}
403+
404+
} // namespace
405+
406+
static PyObject *SpanBegin(PyObject *self, PyObject *)
407+
{
408+
auto begin = spanBegin();
409+
if (!begin)
410+
return nullptr;
411+
return PyObject_CallOneArg(begin, self);
412+
}
413+
414+
static PyObject *SpanEnd(PyObject *self, PyObject *)
415+
{
416+
auto end = spanEnd();
417+
if (!end)
418+
return nullptr;
419+
return PyObject_CallOneArg(end, self);
420+
}
421+
317422
static bool FillVector(PyObject* vecin, PyObject* args, ItemGetter* getter)
318423
{
319424
Py_ssize_t sz = getter->size();
@@ -1832,6 +1937,21 @@ bool CPyCppyy::Pythonize(PyObject* pyclass, const std::string& name)
18321937

18331938
//- class name based pythonization -------------------------------------------
18341939

1940+
if (IsTemplatedSTLClass(name, "span")) {
1941+
// libstdc++ (GCC >= 15) implements std::span::iterator using a private
1942+
// nested tag type, which makes the iterator non-instantiable by
1943+
// CallFunc-generated wrappers (the return type cannot be named without
1944+
// violating access rules).
1945+
//
1946+
// To preserve correct Python iteration semantics, we replace begin()/end()
1947+
// for std::span to return a custom pointer-based iterator instead. This
1948+
// avoids relying on std::span::iterator while still providing a real C++
1949+
// iterator object that CPyCppyy can also wrap and expose via
1950+
// __iter__/__next__.
1951+
Utility::AddToClass(pyclass, "begin", (PyCFunction)SpanBegin, METH_NOARGS);
1952+
Utility::AddToClass(pyclass, "end", (PyCFunction)SpanEnd, METH_NOARGS);
1953+
}
1954+
18351955
if (IsTemplatedSTLClass(name, "vector")) {
18361956

18371957
// std::vector<bool> is a special case in C++

bindings/pyroot/cppyy/cppyy/test/test_stltypes.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2179,5 +2179,33 @@ def test04_from_cpp(self):
21792179
assert cppyy.gbl.GetMyErrorCount() == 0
21802180

21812181

2182+
def has_cpp_20():
2183+
import cppyy
2184+
2185+
return cppyy.gbl.gInterpreter.ProcessLine("__cplusplus;") >= 202002
2186+
2187+
2188+
@mark.skipif(not has_cpp_20(), reason="std::span requires C++20")
2189+
class TestSTLSPAN:
2190+
import cppyy
2191+
2192+
def test01_span_iterators(self):
2193+
"""
2194+
Test that std::span::begin() and std::span::end() can be used.
2195+
2196+
Covers https://github.com/root-project/root/issues/18837
2197+
"""
2198+
import cppyy
2199+
2200+
l1 = [1, 2, 3]
2201+
v = cppyy.gbl.vector(int)(l1)
2202+
s = cppyy.gbl.span(int)(v)
2203+
s.begin()
2204+
s.end()
2205+
# Check that the iteration also works, which uses begin() and end()
2206+
# internally.
2207+
assert [b for b in s] == l1
2208+
2209+
21822210
if __name__ == "__main__":
21832211
exit(pytest.main(args=['-sv', '-ra', __file__]))

0 commit comments

Comments
 (0)