-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
[smart_holder] Bug fix: std::unique_ptr
deleter needs to be copied.
#4850
Conversation
Using a `union` is complicated: https://en.cppreference.com/w/cpp/language/union > If members of a union are classes with user-defined constructors and destructors, to switch the active member, explicit destructor and placement new are generally needed: Using `std::variant` increases compile-time overhead. It is currently unclear how much these effects matter in practice: optimization left for later.
``` -- The CXX compiler identification is Clang 15.0.7 /usr/bin/cmake -E __run_co_compile --tidy="/usr/bin/clang-tidy;--use-color;--warnings-as-errors=*;--extra-arg-before=--driver-mode=g++" --source=/__w/pybind11/pybind11/tests/test_class_sh_inheritance.cpp -- /usr/bin/c++ -DPYBIND11_ENABLE_TYPE_CASTER_ODR_GUARD_IF_AVAILABLE -DPYBIND11_TEST_EIGEN -Dpybind11_tests_EXPORTS -I/__w/pybind11/pybind11/include -isystem /usr/include/python3.9 -isystem /__w/pybind11/pybind11/build/_deps/eigen-src -Os -DNDEBUG -fPIC -fvisibility=hidden -Wall -Wextra -Wconversion -Wcast-qual -Wdeprecated -Wundef -Wnon-virtual-dtor -flto=thin -std=c++17 -o CMakeFiles/pybind11_tests.dir/test_class_sh_inheritance.cpp.o -c /__w/pybind11/pybind11/tests/test_class_sh_inheritance.cpp /__w/pybind11/pybind11/tests/pure_cpp/smart_holder_poc_test.cpp:264:30: error: pointer parameter 'raw_ptr' can be pointer to const [readability-non-const-parameter,-warnings-as-errors] new int(19), [](int *raw_ptr) { delete raw_ptr; }); ^ const ```
…esired (the lambda function only captures a reference, which can become dangling).
``` /usr/bin/cmake -E __run_co_compile --tidy="/usr/bin/clang-tidy;--use-color;--warnings-as-errors=*;--extra-arg-before=--driver-mode=g++" --source=/__w/pybind11/pybind11/tests/test_class.cpp -- /usr/bin/c++ -DPYBIND11_ENABLE_TYPE_CASTER_ODR_GUARD_IF_AVAILABLE -DPYBIND11_TEST_EIGEN -Dpybind11_tests_EXPORTS -I/__w/pybind11/pybind11/include -isystem /usr/include/python3.9 -isystem /__w/pybind11/pybind11/build/_deps/eigen-src -Os -DNDEBUG -fPIC -fvisibility=hidden -Wall -Wextra -Wconversion -Wcast-qual -Wdeprecated -Wundef -Wnon-virtual-dtor -flto=thin -std=c++17 -o CMakeFiles/pybind11_tests.dir/test_class.cpp.o -c /__w/pybind11/pybind11/tests/test_class.cpp /__w/pybind11/pybind11/include/pybind11/detail/smart_holder_poc.h:114:5: error: single-argument constructors must be marked explicit to avoid unintentional implicit conversions [google-explicit-constructor,-warnings-as-errors] custom_deleter(D &&deleter) : deleter{std::move(deleter)} {} ^ explicit /__w/pybind11/pybind11/include/pybind11/detail/smart_holder_poc.h:120:76: error: forwarding reference passed to std::move(), which may unexpectedly cause lvalues to be moved; use std::forward() instead [bugprone-move-forwarding-reference,-warnings-as-errors] return guarded_delete(std::function<void(void *)>(custom_deleter<T, D>(std::move(uqp_del))), ^~~~~~~~~ std::forward<D> ```
Reduced from a PyCLIF use case in the wild by @wangxf123456 (internal change cl/565476030).
Hi @iwanders, I'm replying here to your message under rwgk#26. When I saw your message I decided it's best to first transfer a simple existing internal-only test to a new test_class_sh_unique_ptr_custom_deleter here, so that you see everything that exists already: I was thinking that we probably need more tests in tests/pure_cpp/smart_holder_poc_test.cpp, and expand test_class_sh_unique_ptr_custom_deleter. But I'm very uncertain myself what & how exactly. Things that come to mind as maybe relevant: It's not obvious, but some code in smart_holder_poc.h turned out to not be usable like that from smart_holder_type_casters.h (when I worked on it originally a couple years ago). It's only used by tests/pure_cpp/smart_holder_poc_test.cpp. One of the things I always wanted to do but never got to: move all code that is not used from smart_holder_type_casters.h to the tests/pure_cpp/ directory, so that only the production code is left in smart_holder_poc.h. The test-only code in smart_holder_poc.h is probably more distracting than helpful for your purposes. I was thinking of the smart_holder_poc.h code under this PR as ~complete: it works for Google-internal global testing and solved in-the-wild test failures (17 tests, probably 3-5 unique use cases). Therefore I'm surprised that you're adding code in smart_holder_poc.h. What do you think about this approach to collaborating:
|
Hmm, I may be missing something, but I was assuming a custom deleter should survive a roundtrip from Currently it doesn't:
Github won't let me comment on the appropriate line, but I feel this current PR only fixes the bug in one direction (going into smart holder), not in the direction of going out of the smart holder. This test shows the problem, it will fail with this branch and unmodified code, which is why I was modifying the smart holder file. Edit;
Went through it, overall it looks fine to me, my main concern is the above statement that the deleter is only handled when going into the smart holder, not when being converted back to a unique pointer.
Sure, I can't commit a lot of time, but happy to help over the next week or so. |
} | ||
struct custom_deleter { | ||
D deleter; | ||
explicit custom_deleter(D &&deleter) : deleter{std::move(deleter)} {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is always tricky, but shouldn't this std::move
be a std::forward
instead? THe D&&
could be a universal reference to an lvalue I think, in which case moving it may not have the intended effect at the call site, while forward will move if it is an rvalue but copy if it is an lvalue?
Edit; so make this like line 122.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed, thanks for catching this!
This is always tricky,
Yeah, I wasn't paying enough attention here before.
} // namespace class_sh_unique_ptr_custom_deleter | ||
} // namespace pybind11_tests | ||
|
||
PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_unique_ptr_custom_deleter::Pet) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Feel free to ignore) This line missing a semicolon trips up the syntax highlighting on github, but it seems to be in line with the other tests. Could add a do {} while(0)
at the end of the macro to enforce the ;
at the end of the line. But I think that requires a lot of changes in other unit tests as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Too late", unfortunately: I just looked, I'd have to change 35 files around the Google codebase, outside the pybind11 source directory. It'll set me back a whole day or two worth of effort. And there are so many other far more substantial things wishing for those days.
|
||
def test_create(): | ||
pet = m.create("abc") | ||
assert pet.name == "abc" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This unit test doesn't actually test the deleter is ran, unless ran inside valgrind / lsan, is that intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This unit test doesn't actually test the deleter is ran, unless ran inside valgrind / lsan, is that intentional?
@wangxf123456 was completely focused on reducing a test from the wild, just enough to reproduce the error message we saw.
Copy-pasting what @iwanders wrote in another comment:
my main concern is the above statement that the deleter is only handled when going into the smart holder, not when being converted back to a unique pointer.
I agree. I was also completely focused on the issue in the wild. — Reporting a thought/uncertainty in the back of my mind: Do we actually have to solve this (is that feature actually needed in the wild)? Or would it be sufficient to clearly report an error if someone tried to use the non-existing feature? And then work on it as needed.
I'll add a note about it to the PR description, then merge this PR when I see that the GHA are happy.
Sure, I can't commit a lot of time, but happy to help over the next week or so.
I really appreciate any help! And I'm a big believer in small steps. I want to encourage you to pick out one thing at a time. Solve it. Merge. Next one. (My goal, in general, is to review PRs within hours; highest priority.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or would it be sufficient to clearly report an error if someone tried to use the non-existing feature? And then work on it as needed.
Yeah, tricky, it's a balance. I'd at least throw in case there's a non default deleter detected in as_unique_ptr
, that way it explicitly fails, instead of implicitly losing the deleter and leaking memory, and that should still be doable while avoiding most of the extra code I had to add to account for it.
Speaking of as_unique_ptr
, another random thought; that method could be marked &&
, so std::unique_ptr<T, D> as_unique_ptr() &&
, that ensures it can only be called on an r-value smart_holder
, which makes it clear that the smart_holder
object after the method call is in an indeterminate state. In that case it would be illegal to call as_unique_ptr
on an lvalue though, so it makes it a bit more cumbersome at the callsite, but it does draw more attention to what it is doing. Not saying this should be done, but just stating it is an option to make it clear we are moving out of the smart holder.
I'll add a note about it to the PR description, then merge this PR when I see that the GHA are happy.
Sounds good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see that as_unique_ptr
was also wrapped with PYBIND11_TESTS_PURE_CPP_SMART_HOLDER_POC_TEST_CPP
; 100% agree it's probably not worth worrying about the deleter there then 👍
…e `smart_holder::as_unique_ptr` method from production code.
Side remark: Something weird is happening with this test:
It seems to affect only windows-2022 builds. Un/Related? I need to find out. (There is one test_iostream flake mixed in.) |
…` to mark code that is not used from production code. Add comment to explain.
Hm ... IIUC that's not in line with my thinking behind the code. I wanted to leave the smart_holder in an well-defined state at all times. I believe it has to, because we have to assume it lives in a Python object until that is destroyed. Your very good round-trip question seems much more important in the context of smart_holder_type_casters.h: is that actually working as intended? I'm thinking we need something like these: pybind11/tests/test_class_sh_basic.cpp Lines 58 to 65 in e955753
Only more sophisticated to establish firmly that the deleter is copied properly. |
I think this PR is in good shape for merging now, but I'm mystified by the persistent type_caster_odr test failures with windows-2022. I need to get to the bottom of those first. I'll use that time to rerun Google global testing for this PR, although the only change is the |
Ah, sorry, I worded this ambiguous. You're right it will be in a well defined state, I was mostly concerned about the semantics, to me the
Hmm, still trying to wrap my head around that file as an outsider xD. Easiest way I'd test it is by making a destructor that has a side effect (sets a boolean on call), then perform the round trip and ensure the deleter is only called after the final value goes out of scope. |
Does the `PYBIND11_TESTS_PURE_CPP_SMART_HOLDER_POC_TEST_CPP` define have anything to do with it?
That sounds good! I recommend waiting until this PR is merged, so that we can collaborate more easily. I'll try to get to the bottom of the type_caster_odr_guard failures with highest priority. |
Certainly, the windows-2022 failures are due to changes in the environment, unrelated to this PR: That ran with smart_holder head, without any code changes. 6 windows-2022 failures, 5 are type_caster_odr_guard failures, one test_iostream flake (which is very common). I'll revert my latest commit here, then merge. I'll work on dealing with the type_caster_odr_guard failures separately. |
This reverts commit fe59369.
For easy future reference, global testing internal ID: OCL:566559299:BASE:578853944:1698937167027:61af92f8 (passed) |
@iwanders FYI I'll merge from master into smart_holder, too. Should be done in an hour or so. Doesn't change much I guess, just so you're not surprised. |
Done. |
@rwgk , thanks, I haven't had time to look at it yet, but wanted to let you know I'll try to make that deleter roundtrip unit test over the weekend. I have to take some time to read up on how the smart holder actually works and how it interacts with unique pointers or deleters first :) |
I'd be happy to meet and go through the code together. If you're interested, please get in touch via email (in git log). (I looked but couldn't find your direct email.) |
Description
IMPORTANT: Quite possibly follow-on work is needed. See comments for details, in particular #4850 (comment) below.
Root cause for this bug: Firmly held wrong belief that
std::unique_ptr
does not store a deleter.But here it is:
Bug detected via a few failing tests with PyCLIF-pybind11 (out of many millions).
Suggested changelog entry: