Skip to content

Commit ee0c5ee

Browse files
authored
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
1 parent 077a16e commit ee0c5ee

File tree

4 files changed

+176
-32
lines changed

4 files changed

+176
-32
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: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1955,25 +1955,52 @@ 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.
1970+
template <typename Iterator>
1971+
struct iterator_access {
1972+
using result_type = decltype((*std::declval<Iterator>()));
1973+
// NOLINTNEXTLINE(readability-const-return-type) // PR #3263
1974+
result_type operator()(Iterator &it) const {
1975+
return *it;
1976+
}
1977+
};
19661978

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,
1979+
template <typename Iterator>
1980+
struct iterator_key_access {
1981+
using result_type = decltype(((*std::declval<Iterator>()).first));
1982+
result_type operator()(Iterator &it) const {
1983+
return (*it).first;
1984+
}
1985+
};
1986+
1987+
template <typename Iterator>
1988+
struct iterator_value_access {
1989+
using result_type = decltype(((*std::declval<Iterator>()).second));
1990+
result_type operator()(Iterator &it) const {
1991+
return (*it).second;
1992+
}
1993+
};
1994+
1995+
template <typename Access,
1996+
return_value_policy Policy,
19691997
typename Iterator,
19701998
typename Sentinel,
1971-
#ifndef DOXYGEN_SHOULD_SKIP_THIS // Issue in breathe 4.26.1
1972-
typename ValueType = decltype(*std::declval<Iterator>()),
1973-
#endif
1999+
typename ValueType,
19742000
typename... Extra>
1975-
iterator make_iterator(Iterator first, Sentinel last, Extra &&... extra) {
1976-
using state = detail::iterator_state<Iterator, Sentinel, false, Policy>;
2001+
iterator make_iterator_impl(Iterator first, Sentinel last, Extra &&... extra) {
2002+
using state = detail::iterator_state<Access, Policy, Iterator, Sentinel, ValueType, Extra...>;
2003+
// TODO: state captures only the types of Extra, not the values
19772004

19782005
if (!detail::get_type_info(typeid(state), false)) {
19792006
class_<state>(handle(), "iterator", pybind11::module_local())
@@ -1987,43 +2014,63 @@ iterator make_iterator(Iterator first, Sentinel last, Extra &&... extra) {
19872014
s.first_or_done = true;
19882015
throw stop_iteration();
19892016
}
1990-
return *s.it;
2017+
return Access()(s.it);
19912018
// NOLINTNEXTLINE(readability-const-return-type) // PR #3263
19922019
}, std::forward<Extra>(extra)..., Policy);
19932020
}
19942021

19952022
return cast(state{first, last, true});
19962023
}
19972024

1998-
/// Makes an python iterator over the keys (`.first`) of a iterator over pairs from a
2025+
PYBIND11_NAMESPACE_END(detail)
2026+
2027+
/// Makes a python iterator from a first and past-the-end C++ InputIterator.
2028+
template <return_value_policy Policy = return_value_policy::reference_internal,
2029+
typename Iterator,
2030+
typename Sentinel,
2031+
typename ValueType = typename detail::iterator_access<Iterator>::result_type,
2032+
typename... Extra>
2033+
iterator make_iterator(Iterator first, Sentinel last, Extra &&... extra) {
2034+
return detail::make_iterator_impl<
2035+
detail::iterator_access<Iterator>,
2036+
Policy,
2037+
Iterator,
2038+
Sentinel,
2039+
ValueType,
2040+
Extra...>(first, last, std::forward<Extra>(extra)...);
2041+
}
2042+
2043+
/// Makes a python iterator over the keys (`.first`) of a iterator over pairs from a
19992044
/// first and past-the-end InputIterator.
20002045
template <return_value_policy Policy = return_value_policy::reference_internal,
20012046
typename Iterator,
20022047
typename Sentinel,
2003-
#ifndef DOXYGEN_SHOULD_SKIP_THIS // Issue in breathe 4.26.1
2004-
typename KeyType = decltype((*std::declval<Iterator>()).first),
2005-
#endif
2048+
typename KeyType = typename detail::iterator_key_access<Iterator>::result_type,
20062049
typename... Extra>
20072050
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-
}
2051+
return detail::make_iterator_impl<
2052+
detail::iterator_key_access<Iterator>,
2053+
Policy,
2054+
Iterator,
2055+
Sentinel,
2056+
KeyType,
2057+
Extra...>(first, last, std::forward<Extra>(extra)...);
2058+
}
20252059

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

20292076
/// Makes an iterator over values of an stl container or other container supporting
@@ -2040,6 +2087,13 @@ template <return_value_policy Policy = return_value_policy::reference_internal,
20402087
return make_key_iterator<Policy>(std::begin(value), std::end(value), extra...);
20412088
}
20422089

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

tests/test_sequences_and_iterators.cpp

Lines changed: 58 additions & 0 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
template<typename T>
2021
class NonZeroIterator {
@@ -32,6 +33,29 @@ bool operator==(const NonZeroIterator<std::pair<A, B>>& it, const NonZeroSentine
3233
return !(*it).first || !(*it).second;
3334
}
3435

36+
class NonCopyableInt {
37+
public:
38+
explicit NonCopyableInt(int value) : value_(value) {}
39+
NonCopyableInt(const NonCopyableInt &) = delete;
40+
NonCopyableInt(NonCopyableInt &&other) noexcept : value_(other.value_) {
41+
other.value_ = -1; // detect when an unwanted move occurs
42+
}
43+
NonCopyableInt &operator=(const NonCopyableInt &) = delete;
44+
NonCopyableInt &operator=(NonCopyableInt &&other) noexcept {
45+
value_ = other.value_;
46+
other.value_ = -1; // detect when an unwanted move occurs
47+
return *this;
48+
}
49+
int get() const { return value_; }
50+
void set(int value) { value_ = value; }
51+
~NonCopyableInt() = default;
52+
private:
53+
int value_;
54+
};
55+
using NonCopyableIntPair = std::pair<NonCopyableInt, NonCopyableInt>;
56+
PYBIND11_MAKE_OPAQUE(std::vector<NonCopyableInt>);
57+
PYBIND11_MAKE_OPAQUE(std::vector<NonCopyableIntPair>);
58+
3559
template <typename PythonType>
3660
py::list test_random_access_iterator(PythonType x) {
3761
if (x.size() < 5)
@@ -271,6 +295,10 @@ TEST_SUBMODULE(sequences_and_iterators, m) {
271295
.def(
272296
"items",
273297
[](const StringMap &map) { return py::make_iterator(map.begin(), map.end()); },
298+
py::keep_alive<0, 1>())
299+
.def(
300+
"values",
301+
[](const StringMap &map) { return py::make_value_iterator(map.begin(), map.end()); },
274302
py::keep_alive<0, 1>());
275303

276304
// test_generalized_iterators
@@ -289,8 +317,38 @@ TEST_SUBMODULE(sequences_and_iterators, m) {
289317
.def("nonzero_keys", [](const IntPairs& s) {
290318
return py::make_key_iterator(NonZeroIterator<std::pair<int, int>>(s.begin()), NonZeroSentinel());
291319
}, py::keep_alive<0, 1>())
320+
.def("nonzero_values", [](const IntPairs& s) {
321+
return py::make_value_iterator(NonZeroIterator<std::pair<int, int>>(s.begin()), NonZeroSentinel());
322+
}, py::keep_alive<0, 1>())
292323
;
293324

325+
// test_iterater_referencing
326+
py::class_<NonCopyableInt>(m, "NonCopyableInt")
327+
.def(py::init<int>())
328+
.def("set", &NonCopyableInt::set)
329+
.def("__int__", &NonCopyableInt::get)
330+
;
331+
py::class_<std::vector<NonCopyableInt>>(m, "VectorNonCopyableInt")
332+
.def(py::init<>())
333+
.def("append", [](std::vector<NonCopyableInt> &vec, int value) {
334+
vec.emplace_back(value);
335+
})
336+
.def("__iter__", [](std::vector<NonCopyableInt> &vec) {
337+
return py::make_iterator(vec.begin(), vec.end());
338+
})
339+
;
340+
py::class_<std::vector<NonCopyableIntPair>>(m, "VectorNonCopyableIntPair")
341+
.def(py::init<>())
342+
.def("append", [](std::vector<NonCopyableIntPair> &vec, const std::pair<int, int> &value) {
343+
vec.emplace_back(NonCopyableInt(value.first), NonCopyableInt(value.second));
344+
})
345+
.def("keys", [](std::vector<NonCopyableIntPair> &vec) {
346+
return py::make_key_iterator(vec.begin(), vec.end());
347+
})
348+
.def("values", [](std::vector<NonCopyableIntPair> &vec) {
349+
return py::make_value_iterator(vec.begin(), vec.end());
350+
})
351+
;
294352

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

tests/test_sequences_and_iterators.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def test_generalized_iterators():
2525
assert list(m.IntPairs([(1, 2), (2, 0), (0, 3), (4, 5)]).nonzero_keys()) == [1]
2626
assert list(m.IntPairs([(0, 3), (1, 2), (3, 4)]).nonzero_keys()) == []
2727

28+
assert list(m.IntPairs([(1, 2), (3, 4), (0, 5)]).nonzero_values()) == [2, 4]
29+
assert list(m.IntPairs([(1, 2), (2, 0), (0, 3), (4, 5)]).nonzero_values()) == [2]
30+
assert list(m.IntPairs([(0, 3), (1, 2), (3, 4)]).nonzero_values()) == []
31+
2832
# __next__ must continue to raise StopIteration
2933
it = m.IntPairs([(0, 0)]).nonzero()
3034
for _ in range(3):
@@ -37,6 +41,30 @@ def test_generalized_iterators():
3741
next(it)
3842

3943

44+
def test_iterator_referencing():
45+
"""Test that iterators reference rather than copy their referents."""
46+
vec = m.VectorNonCopyableInt()
47+
vec.append(3)
48+
vec.append(5)
49+
assert [int(x) for x in vec] == [3, 5]
50+
# Increment everything to make sure the referents can be mutated
51+
for x in vec:
52+
x.set(int(x) + 1)
53+
assert [int(x) for x in vec] == [4, 6]
54+
55+
vec = m.VectorNonCopyableIntPair()
56+
vec.append([3, 4])
57+
vec.append([5, 7])
58+
assert [int(x) for x in vec.keys()] == [3, 5]
59+
assert [int(x) for x in vec.values()] == [4, 7]
60+
for x in vec.keys():
61+
x.set(int(x) + 1)
62+
for x in vec.values():
63+
x.set(int(x) + 10)
64+
assert [int(x) for x in vec.keys()] == [4, 6]
65+
assert [int(x) for x in vec.values()] == [14, 17]
66+
67+
4068
def test_sliceable():
4169
sliceable = m.Sliceable(100)
4270
assert sliceable[::] == (0, 100, 1)
@@ -140,6 +168,7 @@ def test_map_iterator():
140168
assert sm[k] == expected[k]
141169
for k, v in sm.items():
142170
assert v == expected[k]
171+
assert list(sm.values()) == [expected[k] for k in sm]
143172

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

0 commit comments

Comments
 (0)