Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add make_value_iterator #3271

Merged
merged 12 commits into from
Sep 21, 2021
Merged

Add make_value_iterator #3271

merged 12 commits into from
Sep 21, 2021

Conversation

bmerry
Copy link
Contributor

@bmerry bmerry commented Sep 15, 2021

Description

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.

Suggested changelog entry:

- Add ``make_value_iterator()``.
- Fix ``make_key_iterator()`` to return references instead of copies.

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.

if (!detail::get_type_info(typeid(state), false)) {
class_<state>(handle(), "iterator", pybind11::module_local())
.def("__iter__", [](state &s) -> state& { return s; })
.def("__next__", [](state &s) -> ValueType {
.def("__next__", [](state &s) -> detail::remove_cv_t<ValueType> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rwgk I think we need to make sure that this doesnt' cause those edge case issues we were having with removing the const in return types for Eigen

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could run this through our global testing tonight (after I got a chance to take a closer look at the code).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Skylion007 could you elaborate on what those problems were? Is it something that might only show up when iterating over a container of Eigen objects?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Long story short, Eigen has a special caster that makes it so const Eigen objects are to Numpy arrays with the writable flag off, while non-const Eigen objects have them on so this can actually break some tests even though it's against C++ guidelines / behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what would a test for that look like? Something like binding std::vector<const Eigen::Vector3f>, passing it to Python and checking that the writeable flag is off when iterating it? For that matter I'm not even sure it's valid to have a const value type in an STL container.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be on the safe side I've removed all the remove_cv_t stuff and put in NOLINTNEXTLINE instead. I also discovered that subtleties in decltype meant in master, make_key_iterator was making copies rather than references. I think that should be fixed in this PR (it's more important for make_value_iterator, since map values are mutable while keys are not), but I still need to add some tests.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we add a unit test asserting the key is not copied (e.g. delete the copy constructor)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we add a unit test asserting the key is not copied (e.g. delete the copy constructor)?

That's exactly the test I've added. I just need to see why it's failing on some compilers in the CI matrix but not others.

@rwgk
Copy link
Collaborator

rwgk commented Sep 16, 2021

I'm seeing ~15 breakages (out of a huge number) in the Google-internal global testing (which includes a large number of open source packages). Spot-checking, all failures appear to be similar to the error below.

I applied the changes on top of the smart_holder branch (the only way I can test run the global tests), but I don't think that makes a difference with regards to the breakages, except that the pybind11.h line numbers below don't match what's on master. I captured the relevant source code fragments below.

The third_party code is from here:
https://github.com/IntelRealSense/librealsense/releases/tag/v2.38.1
An exact source code link is below.

Please let me know any suggestions or ideas to try out. I'm traveling today/tomorrow but will try to respond as much as possible.

In file included from third_party/librealsense2/v2_38_1/wrappers/python/pyrs_frame.cpp:4:
In file included from third_party/librealsense2/v2_38_1/wrappers/python/python.hpp:7:
third_party/pybind11/include/pybind11/pybind11.h:2089:16: error: indirection requires pointer operand ('const rs2::frameset::iterator' invalid)
        return *it;
               ^~~
third_party/pybind11/include/pybind11/pybind11.h:2129:24: note: in instantiation of member function 'pybind11::detail::iterator_access<rs2::frameset::iterator>::operator()' requested here
                return Access()(s.it);
                       ^
third_party/pybind11/include/pybind11/pybind11.h:2147:20: note: in instantiation of function template specialization 'pybind11::detail::make_iterator_impl<pybind11::detail::iterator_access<rs2::frameset::iterator>, pybind11::return_value_policy::reference_internal, rs2::frameset::iterator, rs2::frameset::iterator, rs2::frame>' requested here
    return detail::make_iterator_impl<
                   ^
third_party/librealsense2/v2_38_1/wrappers/python/pyrs_frame.cpp:267:24: note: in instantiation of function template specialization 'pybind11::make_iterator<pybind11::return_value_policy::reference_internal, rs2::frameset::iterator, rs2::frameset::iterator, rs2::frame>' requested here
            return py::make_iterator(self.begin(), self.end());
                       ^
1 error generated.

pybind11.h fragments, in order as reference by the error message:

template <typename Iterator>                                                    
struct iterator_access {                                                        
    detail::remove_cv_t<decltype(*std::declval<Iterator>())> operator()(const Iterator it) {
        return *it;                                                             
    }                                                                           
};                                                                              
template <typename Access,                                                      
          return_value_policy Policy,                                           
          typename Iterator,                                                    
          typename Sentinel,                                                    
          typename ValueType,                                                   
          typename... Extra>                                                    
iterator make_iterator_impl(Iterator first, Sentinel last, Extra &&... extra) { 
    using state = detail::iterator_state<Access, Policy, Iterator, Sentinel, ValueType, Extra...>;
    // TODO: state captures only the types of Extra, not the values             
                                                                                
    if (!detail::get_type_info(typeid(state), false)) {                         
        class_<state>(handle(), "iterator", pybind11::module_local())           
            .def("__iter__", [](state &s) -> state& { return s; })              
            .def("__next__", [](state &s) -> detail::remove_cv_t<ValueType> {   
                if (!s.first_or_done)                                           
                    ++s.it;                                                     
                else                                                            
                    s.first_or_done = false;                                    
                if (s.it == s.end) {                                            
                    s.first_or_done = true;                                     
                    throw stop_iteration();                                     
                }                                                               
                return Access()(s.it);                                          
            }, std::forward<Extra>(extra)..., Policy);                          
    }                                                                           
                                                                                
    return cast(state{first, last, true});                                      
}
/// Makes a python iterator from a first and past-the-end C++ InputIterator.    
template <return_value_policy Policy = return_value_policy::reference_internal, 
          typename Iterator,                                                    
          typename Sentinel,                                                    
#ifndef DOXYGEN_SHOULD_SKIP_THIS  // Issue in breathe 4.26.1                    
          typename ValueType = decltype(*std::declval<Iterator>()),             
#endif                                                                          
          typename... Extra>                                                    
iterator make_iterator(Iterator first, Sentinel last, Extra &&... extra) {      
    return detail::make_iterator_impl<                                          
        detail::iterator_access<Iterator>,                                      
        Policy,                                                                 
        Iterator,                                                               
        Sentinel,                                                               
        ValueType,                                                              
        Extra...>(first, last, std::forward<Extra>(extra)...);                  
}

third_party/librealsense2/v2_38_1/wrappers/python/pyrs_frame.cpp

https://github.com/IntelRealSense/librealsense/blob/f7ff3715b3e6c48cba96db816105523876e2ba1b/wrappers/python/pyrs_frame.cpp#L267

        .def("__iter__", [](rs2::frameset& self) {                              
            return py::make_iterator(self.begin(), self.end());                 
        }, py::keep_alive<0, 1>()) 

@bmerry
Copy link
Contributor Author

bmerry commented Sep 16, 2021

Thanks for that test @rwgk. Looking at the code again I see that for some reason I made the iterator_access helper and friends take the parameter at const Iterator, which I think I probably meant to be const Iterator &. But based on the error I suspect it actually needs to be Iterator &, for the case where the iterator's operator* is not const. I'm not sure if that's legal other than for an output iterator (the C++20 input_iterator concept requires indirectly_readable, which requires it to be const, but C++20 also defines legacy/C++17 iterator concepts which makes the whole thing very confusing), but we should still try to keep existing code working.

For some reason I'd accidentally made it take a const value, which
caused some issues with third-party packages.
@bmerry
Copy link
Contributor Author

bmerry commented Sep 17, 2021

@rwgk I've changed the iterator_access classes in a way that I hope will fix things. Could you re-run your battery of tests?

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.
This is favoured over using remove_cv_t because in some cases a const
value return type is deliberate (particularly for Eigen).
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.
@bmerry
Copy link
Contributor Author

bmerry commented Sep 17, 2021

@rwgk I think don't run the Google tests quite yet - there are still some CI failures I need to understand, which seem to be indicating that some compilers (but not all) are trying to make copies where I'm not expecting it. I'll have to investigate next week.

@rainwoodman
Copy link

std::iterator and std::pair have ::value_type and ::first_type, ::second_type:
https://www.cplusplus.com/reference/utility/pair/?kw=pair

I guess the reason to use decltype is that these type names are not always defined in non-std iterator and pairs?

@bmerry
Copy link
Contributor Author

bmerry commented Sep 17, 2021

I guess the reason to use decltype is that these type names are not always defined in non-std iterator and pairs?

Iterators aren't required to provide value_type (for example, pointers are iterators). One could use iterator_traits but frankly decltype is simpler. A lot of these typedefs pre-date C++11 and decltype and so were necessary at the time.

An advantage of decltype is that it can be more accurate for things like references and constness. Consider decltype(((*it).first)). If it is a const pair<int, int> *, then decltype gives const int &, whereas first_type is int.

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.
@bmerry
Copy link
Contributor Author

bmerry commented Sep 18, 2021

It looks like at a minimum ICC is buggy when it comes to decltype in a template default: https://godbolt.org/z/dfeanhYa9

Another of the failures is with PGI, but godbolt.org doesn't support PGI so I can't directly probe the issue. I'm going to try some changes to see if I can work around the problem, but it'll be partly trial-and-error since I'll have to test via the CI.

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.
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.
@bmerry
Copy link
Contributor Author

bmerry commented Sep 19, 2021

I've finally gotten the CI to all pass. @rwgk could you try the Google testing again?

Comment on lines 2046 to 2048
#ifndef DOXYGEN_SHOULD_SKIP_THIS // Issue in breathe 4.26.1
typename KeyType = decltype((*std::declval<Iterator>()).first),
typename KeyType = typename detail::iterator_key_access<Iterator>::result_type,
#endif
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Skylion007 I don't know what this breathe issue was, but maybe my change will resolve it? I can try removing the #ifndef but I don't know what symptoms to look for.

Copy link
Collaborator

@Skylion007 Skylion007 Sep 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bmerry From looking at the git blame, the issue is further described here: #2828 . It appears as though not skipping this will hide the output from the reference documentation: #2826 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. If I've followed the chain of causes, I think it's probably no longer an issue (I've run into the same issue with Doxygen myself, where it eats parentheses from decltype expressions), so I've deleted the DOXYGEN_SHOULD_SKIP_THIS guards and we'll see what the CI says.

Copy link
Collaborator

@Skylion007 Skylion007 Sep 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bmerry Also, we could consider bumping Breathe if needed. It's currently pinned to that version via requirements.

@rwgk
Copy link
Collaborator

rwgk commented Sep 19, 2021

FYI: I initiated new Google testing. It'll take ~8 hours before the results are in.

I verified interactively that the latest version of this PR resolves the realsense breakage reported here before.

@Skylion007
Copy link
Collaborator

Skylion007 commented Sep 19, 2021

Interestingly, this small change increases the binary size by 28kb, I am guess it's the new STL wrappers for the new test cases, just want to make sure our clever new templates and decl aren't adding unnecessary bloat. For a 1% size regression, this diff seems worth it IMO.

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.
@bmerry
Copy link
Contributor Author

bmerry commented Sep 19, 2021

Interestingly, this small change increases the binary size by 28kb, I am guess it's the new STL wrappers for the new test cases, just want to make sure our clever new templates and decl aren't adding unnecessary bloat. For a 1% size regression, this diff seems worth it IMO.

I don't have any experience trying to optimise for size, or even know what exactly you're measuring. Can you perhaps try compiling the tests from master against the pybind11.h from this branch? That should tell us what the impact of the restructuring is without the noise from adding more test cases.

Also if you're testing this, can you try declaring make_iterator_impl static? That might help the compiler to inline it into it into the implementations of make_iterator and make_key_iterator and not emit out-of-line copies, but I confess to being a little hazy how linkage works with templates so maybe it won't help.

@Skylion007
Copy link
Collaborator

I don't have a lot of experience optimizing it either. Our cmake just outputs the size of shared libraries and I wanted to make note of it in case there was an easy fix.

@bmerry
Copy link
Contributor Author

bmerry commented Sep 19, 2021

I don't have a lot of experience optimizing it either. Our cmake just outputs the size of shared libraries and I wanted to make note of it in case there was an easy fix.

If you send me the command you run to build and where to look in the output I can try the suggestions I made.

@Skylion007
Copy link
Collaborator

cmake -S. -B build   -DDOWNLOAD_EIGEN=ON        -DDOWNLOAD_CATCH=ON        -DCMAKE_CXX_STANDARD=17 
cmake --build build 

Should be the one of the last outputs of cmake

@rwgk
Copy link
Collaborator

rwgk commented Sep 19, 2021

cmake -S. -B build   -DDOWNLOAD_EIGEN=ON        -DDOWNLOAD_CATCH=ON        -DCMAKE_CXX_STANDARD=17 
cmake --build build 

Should be the one of the last outputs of cmake

It's not very obvious (it escaped my attention for months): you're looking for the output of this line in tools/libsize.py:

print("------", os.path.basename(lib), "file size:", libsize, end="")

@bmerry
Copy link
Contributor Author

bmerry commented Sep 19, 2021

Thanks, I found the file size in the output. On my setup at least, the difference is entirely down to the new tests: if I revert the test code then the size is exactly the same as master. Also adding static to make_iterator_impl make no difference. So if the Google tests pass this should be good to go.

@rwgk
Copy link
Collaborator

rwgk commented Sep 20, 2021

The Google testing was successful.

I didn't actually review this PR. @rainwoodman offered to help with the review.

@Skylion007
Copy link
Collaborator

FYI @bmerry : ee0c5ee#r56899065

@henryiii
Copy link
Collaborator

henryiii commented Sep 22, 2021

It's a little hacky, but here's a MWE:

diff --git a/tests/test_sequences_and_iterators.cpp b/tests/test_sequences_and_iterators.cpp
index 66d64726..faa506c6 100644
--- a/tests/test_sequences_and_iterators.cpp
+++ b/tests/test_sequences_and_iterators.cpp
@@ -306,6 +306,7 @@ TEST_SUBMODULE(sequences_and_iterators, m) {
     public:
         explicit IntPairs(std::vector<std::pair<int, int>> data) : data_(std::move(data)) {}
         const std::pair<int, int>* begin() const { return data_.data(); }
+        const std::pair<int, int>* end() const { return data_.data() + data_.size(); }
     private:
         std::vector<std::pair<int, int>> data_;
     };
@@ -320,6 +321,10 @@ TEST_SUBMODULE(sequences_and_iterators, m) {
         .def("nonzero_values", [](const IntPairs& s) {
             return py::make_value_iterator(NonZeroIterator<std::pair<int, int>>(s.begin()), NonZeroSentinel());
         }, py::keep_alive<0, 1>())
+        // test iterator with keepalive
+        .def("make_iterator_keep_alive", [](IntPairs& self) {
+            return py::make_iterator(self, py::keep_alive<0, 1>());
+        }, py::keep_alive<0, 1>())
         ;

     // test_iterater_referencing
@@ -430,3 +435,4 @@ TEST_SUBMODULE(sequences_and_iterators, m) {
     m.def("make_iterator_1", []() { return py::make_iterator<py::return_value_policy::copy>(list); });
     m.def("make_iterator_2", []() { return py::make_iterator<py::return_value_policy::automatic>(list); });
 }
+
diff --git a/tests/test_sequences_and_iterators.py b/tests/test_sequences_and_iterators.py
index 2c73eff2..89e42ce0 100644
--- a/tests/test_sequences_and_iterators.py
+++ b/tests/test_sequences_and_iterators.py
@@ -21,6 +21,10 @@ def test_generalized_iterators():
     assert list(m.IntPairs([(1, 2), (2, 0), (0, 3), (4, 5)]).nonzero()) == [(1, 2)]
     assert list(m.IntPairs([(0, 3), (1, 2), (3, 4)]).nonzero()) == []

+    with pytest.raises(RuntimeError):
+        m.IntPairs([(1, 2), (3, 4), (0, 5)]).make_iterator_keep_alive()
+
     assert list(m.IntPairs([(1, 2), (3, 4), (0, 5)]).nonzero_keys()) == [1, 3]
     assert list(m.IntPairs([(1, 2), (2, 0), (0, 3), (4, 5)]).nonzero_keys()) == [1]
     assert list(m.IntPairs([(0, 3), (1, 2), (3, 4)]).nonzero_keys()) == []

This doesn't work at runtime in HEAD^, since it can't do the keep_alive, but it now fails at compile time in HEAD.

FAILED: tests/CMakeFiles/pybind11_tests.dir/test_sequences_and_iterators.cpp.o
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -DBOOST_ALL_NO_LIB -DPYBIND11_TEST_BOOST -DPYBIND11_TEST_EIGEN -Dpybind11_tests_EXPORTS -I/Users/henryschreiner/git/software/pybind11/include -isystem /usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/include/python3.9 -isystem /Users/henryschreiner/git/software/pybind11/.nox/tests-3-9/tmp/_deps/eigen-src -isystem /usr/local/include -Os -DNDEBUG -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.3.sdk -fPIC -fvisibility=hidden -Wall -Wextra -Wconversion -Wcast-qual -Wdeprecated -Wundef -Wnon-virtual-dtor -Werror -flto=thin -std=gnu++11 -MD -MT tests/CMakeFiles/pybind11_tests.dir/test_sequences_and_iterators.cpp.o -MF tests/CMakeFiles/pybind11_tests.dir/test_sequences_and_iterators.cpp.o.d -o tests/CMakeFiles/pybind11_tests.dir/test_sequences_and_iterators.cpp.o -c /Users/henryschreiner/git/software/pybind11/tests/test_sequences_and_iterators.cpp
In file included from /Users/henryschreiner/git/software/pybind11/tests/test_sequences_and_iterators.cpp:11:
In file included from /Users/henryschreiner/git/software/pybind11/tests/pybind11_tests.h:3:
/Users/henryschreiner/git/software/pybind11/include/pybind11/pybind11.h:1972:35: error: indirection requires pointer operand ('IntPairs' invalid)
    using result_type = decltype((*std::declval<Iterator>()));
                                  ^~~~~~~~~~~~~~~~~~~~~~~~~
/Users/henryschreiner/git/software/pybind11/include/pybind11/pybind11.h:2031:49: note: in instantiation of template class 'pybind11::detail::iterator_access<IntPairs>' requested here
          typename ValueType = typename detail::iterator_access<Iterator>::result_type,
                                                ^
/Users/henryschreiner/git/software/pybind11/include/pybind11/pybind11.h:2033:10: note: in instantiation of default argument for 'make_iterator<pybind11::return_value_policy::reference_internal, IntPairs, pybind11::keep_alive<0, 1>>' required here
iterator make_iterator(Iterator first, Sentinel last, Extra &&... extra) {
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/Users/henryschreiner/git/software/pybind11/tests/test_sequences_and_iterators.cpp:326:20: note: while substituting deduced template arguments into function template 'make_iterator' [with Policy = (no value), Iterator = IntPairs, Sentinel = pybind11::keep_alive<0, 1>, ValueType = (no value), Extra = <>]
            return py::make_iterator(self, py::keep_alive<0, 1>());
                   ^
1 error generated.

@rainwoodman
Copy link

So in this reproducer, first=self, last=pybind::keep_alive -- would make sense to use a keep_alive as a sentinel?

@henryiii
Copy link
Collaborator

No, sentinels are something you stop loops with, so need to be deref'd to something comparable to the iterator. This final overload is now unreachable, you can't trigger it if you have any Extras, since everything now matches Sentinel, SFINAE is broken after this PR. We just never tested the final overload with an Extra before.

@bmerry
Copy link
Contributor Author

bmerry commented Sep 22, 2021

Thanks for the MWE @henryiii. I should have time for this next week and I can add that MWE to a follow-up PR. In theory it shouldn't be too hard to fix - it's mostly going to be trying to navigate around compiler bug(s), which means the slow painful process of debug-via-CI.

henryiii added a commit to henryiii/pybind11 that referenced this pull request Sep 22, 2021
henryiii added a commit that referenced this pull request Sep 23, 2021
henryiii added a commit that referenced this pull request Sep 23, 2021
henryiii pushed a commit to henryiii/pybind11 that referenced this pull request Sep 23, 2021
* 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
henryiii added a commit to henryiii/pybind11 that referenced this pull request Sep 23, 2021
henryiii pushed a commit to henryiii/pybind11 that referenced this pull request Sep 23, 2021
* 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
henryiii added a commit to henryiii/pybind11 that referenced this pull request Sep 23, 2021
henryiii pushed a commit to henryiii/pybind11 that referenced this pull request Sep 23, 2021
* 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
henryiii added a commit to henryiii/pybind11 that referenced this pull request Sep 23, 2021
henryiii added a commit to henryiii/pybind11 that referenced this pull request Sep 23, 2021
henryiii added a commit to henryiii/pybind11 that referenced this pull request Sep 23, 2021
henryiii added a commit to henryiii/pybind11 that referenced this pull request Sep 23, 2021
henryiii added a commit to henryiii/pybind11 that referenced this pull request Sep 23, 2021
@henryiii henryiii removed the needs changelog Possibly needs a changelog entry label Sep 23, 2021
henryiii added a commit to henryiii/pybind11 that referenced this pull request Sep 23, 2021
henryiii added a commit to henryiii/pybind11 that referenced this pull request Sep 23, 2021
henryiii added a commit that referenced this pull request Sep 23, 2021
* 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 <1963944+bmerry@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants