Skip to content

Commit 21282e6

Browse files
henryiiibmerry
andauthored
feat: reapply fixed version of #3271 (#3293)
* Add make_value_iterator (#3271) * Add make_value_iterator This is the counterpart to make_key_iterator, and will allow implementing a `value` method in `bind_map` (although doing so is left for a subsequent PR). I made a few design changes to reduce copy-and-paste boilerplate. Previously detail::iterator_state had a boolean template parameter to indicate whether it was being used for make_iterator or make_key_iterator. I replaced the boolean with a class that determines how to dereference the iterator. This allows for a generic implementation of `__next__`. I also added the ValueType and Extra... parameters to the iterator_state template args, because I think it was a bug that they were missing: if make_iterator is called twice with different values of these, only the first set has effect (because the state class is only registered once). There is still a potential issue in that the *values* of the extra arguments are latched on the first call, but since most policies are empty classes this should be even less common. * Add some remove_cv_t to appease clang-tidy * Make iterator_access and friends take reference For some reason I'd accidentally made it take a const value, which caused some issues with third-party packages. * Another attempt to remove remove_cv_t from iterators Some of the return types were const (non-reference) types because of the pecularities of decltype: `decltype((*it).first)` is the *declared* type of the member of the pair, rather than the type of the expression. So if the reference type of the iterator is `pair<const int, int> &`, then the decltype is `const int`. Wrapping an extra set of parentheses to form `decltype(((*it).first))` would instead give `const int &`. This means that the existing make_key_iterator actually returns by value from `__next__`, rather than by reference. Since for mapping types, keys are always const, this probably hasn't been noticed, but it will affect make_value_iterator if the Python code tries to mutate the returned objects. I've changed things to use double parentheses so that make_iterator, make_key_iterator and make_value_iterator should now all return the reference type of the iterator. I'll still need to add a test for that; for now I'm just checking whether I can keep Clang-Tidy happy. * Add back some NOLINTNEXTLINE to appease Clang-Tidy This is favoured over using remove_cv_t because in some cases a const value return type is deliberate (particularly for Eigen). * Add a unit test for iterator referencing Ensure that make_iterator, make_key_iterator and make_value_iterator return references to the container elements, rather than copies. The test for make_key_iterator fails to compile on master, which gives me confidence that this branch has fixed it. * Make the iterator_access etc operator() const I'm actually a little surprised it compiled at all given that the operator() is called on a temporary, but I don't claim to fully understand all the different value types in C++11. * Attempt to work around compiler bugs https://godbolt.org/ shows an example where ICC gets the wrong result for a decltype used as the default for a template argument, and CI also showed problems with PGI. This is a shot in the dark to see if it fixes things. * Make a test constructor explicit (Clang-Tidy) * Fix unit test on GCC 4.8.5 It seems to require the arguments to the std::pair constructor to be implicitly convertible to the types in the pair, rather than just requiring is_constructible. * Remove DOXYGEN_SHOULD_SKIP_THIS guards Now that a complex decltype expression has been replaced by a simpler nested type, I'm hoping Doxygen will be able to build it without issues. * Add comment to explain iterator_state template params * fix: regression in #3271 Co-authored-by: Bruce Merry <[email protected]>
1 parent 2a78abf commit 21282e6

File tree

4 files changed

+195
-35
lines changed

4 files changed

+195
-35
lines changed

docs/reference.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ Convenience functions converting to Python types
6363
.. doxygenfunction:: make_key_iterator(Iterator, Sentinel, Extra &&...)
6464
.. doxygenfunction:: make_key_iterator(Type &, Extra&&...)
6565

66+
.. doxygenfunction:: make_value_iterator(Iterator, Sentinel, Extra &&...)
67+
.. doxygenfunction:: make_value_iterator(Type &, Extra&&...)
68+
6669
.. _extras:
6770

6871
Passing extra arguments to ``def`` or ``class_``

include/pybind11/pybind11.h

Lines changed: 88 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1955,25 +1955,54 @@ inline std::pair<decltype(internals::registered_types_py)::iterator, bool> all_t
19551955
return res;
19561956
}
19571957

1958-
template <typename Iterator, typename Sentinel, bool KeyIterator, return_value_policy Policy>
1958+
/* There are a large number of apparently unused template arguments because
1959+
* each combination requires a separate py::class_ registration.
1960+
*/
1961+
template <typename Access, return_value_policy Policy, typename Iterator, typename Sentinel, typename ValueType, typename... Extra>
19591962
struct iterator_state {
19601963
Iterator it;
19611964
Sentinel end;
19621965
bool first_or_done;
19631966
};
19641967

1965-
PYBIND11_NAMESPACE_END(detail)
1968+
// Note: these helpers take the iterator by non-const reference because some
1969+
// iterators in the wild can't be dereferenced when const. C++ needs the extra parens in decltype
1970+
// to enforce an lvalue. The & after Iterator is required for MSVC < 16.9. SFINAE cannot be
1971+
// reused for result_type due to bugs in ICC, NVCC, and PGI compilers. See PR #3293.
1972+
template <typename Iterator, typename SFINAE = decltype((*std::declval<Iterator &>()))>
1973+
struct iterator_access {
1974+
using result_type = decltype((*std::declval<Iterator &>()));
1975+
// NOLINTNEXTLINE(readability-const-return-type) // PR #3263
1976+
result_type operator()(Iterator &it) const {
1977+
return *it;
1978+
}
1979+
};
19661980

1967-
/// Makes a python iterator from a first and past-the-end C++ InputIterator.
1968-
template <return_value_policy Policy = return_value_policy::reference_internal,
1981+
template <typename Iterator, typename SFINAE = decltype(((*std::declval<Iterator &>()).first)) >
1982+
struct iterator_key_access {
1983+
using result_type = decltype(((*std::declval<Iterator &>()).first));
1984+
result_type operator()(Iterator &it) const {
1985+
return (*it).first;
1986+
}
1987+
};
1988+
1989+
template <typename Iterator, typename SFINAE = decltype(((*std::declval<Iterator &>()).second))>
1990+
struct iterator_value_access {
1991+
using result_type = decltype(((*std::declval<Iterator &>()).second));
1992+
result_type operator()(Iterator &it) const {
1993+
return (*it).second;
1994+
}
1995+
};
1996+
1997+
template <typename Access,
1998+
return_value_policy Policy,
19691999
typename Iterator,
19702000
typename Sentinel,
1971-
#ifndef DOXYGEN_SHOULD_SKIP_THIS // Issue in breathe 4.26.1
1972-
typename ValueType = decltype(*std::declval<Iterator>()),
1973-
#endif
2001+
typename ValueType,
19742002
typename... Extra>
1975-
iterator make_iterator(Iterator first, Sentinel last, Extra &&... extra) {
1976-
using state = detail::iterator_state<Iterator, Sentinel, false, Policy>;
2003+
iterator make_iterator_impl(Iterator first, Sentinel last, Extra &&... extra) {
2004+
using state = detail::iterator_state<Access, Policy, Iterator, Sentinel, ValueType, Extra...>;
2005+
// TODO: state captures only the types of Extra, not the values
19772006

19782007
if (!detail::get_type_info(typeid(state), false)) {
19792008
class_<state>(handle(), "iterator", pybind11::module_local())
@@ -1987,43 +2016,63 @@ iterator make_iterator(Iterator first, Sentinel last, Extra &&... extra) {
19872016
s.first_or_done = true;
19882017
throw stop_iteration();
19892018
}
1990-
return *s.it;
2019+
return Access()(s.it);
19912020
// NOLINTNEXTLINE(readability-const-return-type) // PR #3263
19922021
}, std::forward<Extra>(extra)..., Policy);
19932022
}
19942023

19952024
return cast(state{first, last, true});
19962025
}
19972026

1998-
/// Makes an python iterator over the keys (`.first`) of a iterator over pairs from a
2027+
PYBIND11_NAMESPACE_END(detail)
2028+
2029+
/// Makes a python iterator from a first and past-the-end C++ InputIterator.
2030+
template <return_value_policy Policy = return_value_policy::reference_internal,
2031+
typename Iterator,
2032+
typename Sentinel,
2033+
typename ValueType = typename detail::iterator_access<Iterator>::result_type,
2034+
typename... Extra>
2035+
iterator make_iterator(Iterator first, Sentinel last, Extra &&... extra) {
2036+
return detail::make_iterator_impl<
2037+
detail::iterator_access<Iterator>,
2038+
Policy,
2039+
Iterator,
2040+
Sentinel,
2041+
ValueType,
2042+
Extra...>(first, last, std::forward<Extra>(extra)...);
2043+
}
2044+
2045+
/// Makes a python iterator over the keys (`.first`) of a iterator over pairs from a
19992046
/// first and past-the-end InputIterator.
20002047
template <return_value_policy Policy = return_value_policy::reference_internal,
20012048
typename Iterator,
20022049
typename Sentinel,
2003-
#ifndef DOXYGEN_SHOULD_SKIP_THIS // Issue in breathe 4.26.1
2004-
typename KeyType = decltype((*std::declval<Iterator>()).first),
2005-
#endif
2050+
typename KeyType = typename detail::iterator_key_access<Iterator>::result_type,
20062051
typename... Extra>
20072052
iterator make_key_iterator(Iterator first, Sentinel last, Extra &&...extra) {
2008-
using state = detail::iterator_state<Iterator, Sentinel, true, Policy>;
2009-
2010-
if (!detail::get_type_info(typeid(state), false)) {
2011-
class_<state>(handle(), "iterator", pybind11::module_local())
2012-
.def("__iter__", [](state &s) -> state& { return s; })
2013-
.def("__next__", [](state &s) -> detail::remove_cv_t<KeyType> {
2014-
if (!s.first_or_done)
2015-
++s.it;
2016-
else
2017-
s.first_or_done = false;
2018-
if (s.it == s.end) {
2019-
s.first_or_done = true;
2020-
throw stop_iteration();
2021-
}
2022-
return (*s.it).first;
2023-
}, std::forward<Extra>(extra)..., Policy);
2024-
}
2053+
return detail::make_iterator_impl<
2054+
detail::iterator_key_access<Iterator>,
2055+
Policy,
2056+
Iterator,
2057+
Sentinel,
2058+
KeyType,
2059+
Extra...>(first, last, std::forward<Extra>(extra)...);
2060+
}
20252061

2026-
return cast(state{first, last, true});
2062+
/// Makes a python iterator over the values (`.second`) of a iterator over pairs from a
2063+
/// first and past-the-end InputIterator.
2064+
template <return_value_policy Policy = return_value_policy::reference_internal,
2065+
typename Iterator,
2066+
typename Sentinel,
2067+
typename ValueType = typename detail::iterator_value_access<Iterator>::result_type,
2068+
typename... Extra>
2069+
iterator make_value_iterator(Iterator first, Sentinel last, Extra &&...extra) {
2070+
return detail::make_iterator_impl<
2071+
detail::iterator_value_access<Iterator>,
2072+
Policy, Iterator,
2073+
Sentinel,
2074+
ValueType,
2075+
Extra...>(first, last, std::forward<Extra>(extra)...);
20272076
}
20282077

20292078
/// Makes an iterator over values of an stl container or other container supporting
@@ -2040,6 +2089,13 @@ template <return_value_policy Policy = return_value_policy::reference_internal,
20402089
return make_key_iterator<Policy>(std::begin(value), std::end(value), extra...);
20412090
}
20422091

2092+
/// Makes an iterator over the values (`.second`) of a stl map-like container supporting
2093+
/// `std::begin()`/`std::end()`
2094+
template <return_value_policy Policy = return_value_policy::reference_internal,
2095+
typename Type, typename... Extra> iterator make_value_iterator(Type &value, Extra&&... extra) {
2096+
return make_value_iterator<Policy>(std::begin(value), std::end(value), extra...);
2097+
}
2098+
20432099
template <typename InputType, typename OutputType> void implicitly_convertible() {
20442100
struct set_flag {
20452101
bool &flag;

tests/test_sequences_and_iterators.cpp

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
#include <algorithm>
1717
#include <utility>
18+
#include <vector>
1819

1920
#ifdef PYBIND11_HAS_OPTIONAL
2021
#include <optional>
@@ -37,6 +38,29 @@ bool operator==(const NonZeroIterator<std::pair<A, B>>& it, const NonZeroSentine
3738
return !(*it).first || !(*it).second;
3839
}
3940

41+
class NonCopyableInt {
42+
public:
43+
explicit NonCopyableInt(int value) : value_(value) {}
44+
NonCopyableInt(const NonCopyableInt &) = delete;
45+
NonCopyableInt(NonCopyableInt &&other) noexcept : value_(other.value_) {
46+
other.value_ = -1; // detect when an unwanted move occurs
47+
}
48+
NonCopyableInt &operator=(const NonCopyableInt &) = delete;
49+
NonCopyableInt &operator=(NonCopyableInt &&other) noexcept {
50+
value_ = other.value_;
51+
other.value_ = -1; // detect when an unwanted move occurs
52+
return *this;
53+
}
54+
int get() const { return value_; }
55+
void set(int value) { value_ = value; }
56+
~NonCopyableInt() = default;
57+
private:
58+
int value_;
59+
};
60+
using NonCopyableIntPair = std::pair<NonCopyableInt, NonCopyableInt>;
61+
PYBIND11_MAKE_OPAQUE(std::vector<NonCopyableInt>);
62+
PYBIND11_MAKE_OPAQUE(std::vector<NonCopyableIntPair>);
63+
4064
template <typename PythonType>
4165
py::list test_random_access_iterator(PythonType x) {
4266
if (x.size() < 5)
@@ -288,6 +312,10 @@ TEST_SUBMODULE(sequences_and_iterators, m) {
288312
.def(
289313
"items",
290314
[](const StringMap &map) { return py::make_iterator(map.begin(), map.end()); },
315+
py::keep_alive<0, 1>())
316+
.def(
317+
"values",
318+
[](const StringMap &map) { return py::make_value_iterator(map.begin(), map.end()); },
291319
py::keep_alive<0, 1>());
292320

293321
// test_generalized_iterators
@@ -308,19 +336,62 @@ TEST_SUBMODULE(sequences_and_iterators, m) {
308336
.def("nonzero_keys", [](const IntPairs& s) {
309337
return py::make_key_iterator(NonZeroIterator<std::pair<int, int>>(s.begin()), NonZeroSentinel());
310338
}, py::keep_alive<0, 1>())
339+
.def("nonzero_values", [](const IntPairs& s) {
340+
return py::make_value_iterator(NonZeroIterator<std::pair<int, int>>(s.begin()), NonZeroSentinel());
341+
}, py::keep_alive<0, 1>())
342+
343+
// test single-argument make_iterator
311344
.def("simple_iterator", [](IntPairs& self) {
312345
return py::make_iterator(self);
313346
}, py::keep_alive<0, 1>())
314347
.def("simple_keys", [](IntPairs& self) {
315348
return py::make_key_iterator(self);
316349
}, py::keep_alive<0, 1>())
350+
.def("simple_values", [](IntPairs& self) {
351+
return py::make_value_iterator(self);
352+
}, py::keep_alive<0, 1>())
317353

318-
// test iterator with keep_alive (doesn't work so not used at runtime, but tests compile)
319-
.def("make_iterator_keep_alive", [](IntPairs& self) {
320-
return py::make_iterator(self, py::keep_alive<0, 1>());
354+
// Test iterator with an Extra (doesn't do anything useful, so not used
355+
// at runtime, but tests need to be able to compile with the correct
356+
// overload. See PR #3293.
357+
.def("_make_iterator_extras", [](IntPairs& self) {
358+
return py::make_iterator(self, py::call_guard<int>());
359+
}, py::keep_alive<0, 1>())
360+
.def("_make_key_extras", [](IntPairs& self) {
361+
return py::make_key_iterator(self, py::call_guard<int>());
362+
}, py::keep_alive<0, 1>())
363+
.def("_make_value_extras", [](IntPairs& self) {
364+
return py::make_value_iterator(self, py::call_guard<int>());
321365
}, py::keep_alive<0, 1>())
322366
;
323367

368+
// test_iterater_referencing
369+
py::class_<NonCopyableInt>(m, "NonCopyableInt")
370+
.def(py::init<int>())
371+
.def("set", &NonCopyableInt::set)
372+
.def("__int__", &NonCopyableInt::get)
373+
;
374+
py::class_<std::vector<NonCopyableInt>>(m, "VectorNonCopyableInt")
375+
.def(py::init<>())
376+
.def("append", [](std::vector<NonCopyableInt> &vec, int value) {
377+
vec.emplace_back(value);
378+
})
379+
.def("__iter__", [](std::vector<NonCopyableInt> &vec) {
380+
return py::make_iterator(vec.begin(), vec.end());
381+
})
382+
;
383+
py::class_<std::vector<NonCopyableIntPair>>(m, "VectorNonCopyableIntPair")
384+
.def(py::init<>())
385+
.def("append", [](std::vector<NonCopyableIntPair> &vec, const std::pair<int, int> &value) {
386+
vec.emplace_back(NonCopyableInt(value.first), NonCopyableInt(value.second));
387+
})
388+
.def("keys", [](std::vector<NonCopyableIntPair> &vec) {
389+
return py::make_key_iterator(vec.begin(), vec.end());
390+
})
391+
.def("values", [](std::vector<NonCopyableIntPair> &vec) {
392+
return py::make_value_iterator(vec.begin(), vec.end());
393+
})
394+
;
324395

325396
#if 0
326397
// Obsolete: special data structure for exposing custom iterator types to python

tests/test_sequences_and_iterators.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ def test_generalized_iterators():
3636
assert list(m.IntPairs([(1, 2), (2, 0), (0, 3), (4, 5)]).nonzero_keys()) == [1]
3737
assert list(m.IntPairs([(0, 3), (1, 2), (3, 4)]).nonzero_keys()) == []
3838

39+
assert list(m.IntPairs([(1, 2), (3, 4), (0, 5)]).nonzero_values()) == [2, 4]
40+
assert list(m.IntPairs([(1, 2), (2, 0), (0, 3), (4, 5)]).nonzero_values()) == [2]
41+
assert list(m.IntPairs([(0, 3), (1, 2), (3, 4)]).nonzero_values()) == []
42+
3943
# __next__ must continue to raise StopIteration
4044
it = m.IntPairs([(0, 0)]).nonzero()
4145
for _ in range(3):
@@ -55,6 +59,31 @@ def test_generalized_iterators_simple():
5559
(0, 5),
5660
]
5761
assert list(m.IntPairs([(1, 2), (3, 4), (0, 5)]).simple_keys()) == [1, 3, 0]
62+
assert list(m.IntPairs([(1, 2), (3, 4), (0, 5)]).simple_values()) == [2, 4, 5]
63+
64+
65+
def test_iterator_referencing():
66+
"""Test that iterators reference rather than copy their referents."""
67+
vec = m.VectorNonCopyableInt()
68+
vec.append(3)
69+
vec.append(5)
70+
assert [int(x) for x in vec] == [3, 5]
71+
# Increment everything to make sure the referents can be mutated
72+
for x in vec:
73+
x.set(int(x) + 1)
74+
assert [int(x) for x in vec] == [4, 6]
75+
76+
vec = m.VectorNonCopyableIntPair()
77+
vec.append([3, 4])
78+
vec.append([5, 7])
79+
assert [int(x) for x in vec.keys()] == [3, 5]
80+
assert [int(x) for x in vec.values()] == [4, 7]
81+
for x in vec.keys():
82+
x.set(int(x) + 1)
83+
for x in vec.values():
84+
x.set(int(x) + 10)
85+
assert [int(x) for x in vec.keys()] == [4, 6]
86+
assert [int(x) for x in vec.values()] == [14, 17]
5887

5988

6089
def test_sliceable():
@@ -160,6 +189,7 @@ def test_map_iterator():
160189
assert sm[k] == expected[k]
161190
for k, v in sm.items():
162191
assert v == expected[k]
192+
assert list(sm.values()) == [expected[k] for k in sm]
163193

164194
it = iter(m.StringMap({}))
165195
for _ in range(3): # __next__ must continue to raise StopIteration

0 commit comments

Comments
 (0)