This is an archive of the discontinued LLVM Phabricator instance.

[LLVM][Casting.h] Fix dyn_cast for std::unique_ptr.
ClosedPublic

Authored by zuban32 on Apr 10 2023, 9:43 PM.

Details

Summary

Unlike isa<> and cast<>, current implementation of dyn_cast<> fails
to process a std::unique_ptr to a class supporting LLVM RTTI:

// A and B support LLVM RTTI
class A {...}
class B: public A {...}

void foo() {
...

auto V = std::make_unique<A>();
auto VB = dyn_cast<B>(std::move(V));
if (VB)
  ...

...
}

Diff Detail

Event Timeline

zuban32 created this revision.Apr 10 2023, 9:43 PM
Herald added a project: Restricted Project. · View Herald TranscriptApr 10 2023, 9:43 PM
Herald added a subscriber: bzcheeseman. · View Herald Transcript
zuban32 requested review of this revision.Apr 10 2023, 9:43 PM
Herald added a project: Restricted Project. · View Herald TranscriptApr 10 2023, 9:43 PM

Added reviewers based on the previous commits to this file, not sure who should be the right one though, so any review is welcome.

lattner accepted this revision.Apr 11 2023, 11:25 AM

Makes sense. thanks!

This revision is now accepted and ready to land.Apr 11 2023, 11:25 AM
bzcheeseman accepted this revision.Apr 11 2023, 11:37 AM
bzcheeseman added inline comments.
llvm/include/llvm/Support/Casting.h
357

I think you want std::forward<std::unique_ptr<From>>(f)? - see here:

https://en.cppreference.com/w/cpp/utility/forward

For example, if used in a wrapper such as the following, the template behaves as described below:
template<class T>
void wrapper(T&& arg)
{
    // arg is always lvalue
    foo(std::forward<T>(arg)); // Forward as lvalue or as rvalue, depending on T
}
If a call to wrapper() passes an rvalue std::string, then T is deduced to std::string (not std::string&, const std::string&, or std::string&&), and std::forward ensures that an rvalue reference is passed to foo.
If a call to wrapper() passes a const lvalue std::string, then T is deduced to const std::string&, and std::forward ensures that a const lvalue reference is passed to foo.
If a call to wrapper() passes a non-const lvalue std::string, then T is deduced to std::string&, and std::forward ensures that a non-const lvalue reference is passed to foo.

Are there unit tests you could add to this to ensure we don't regress? Clearly we don't have good enough coverage on this.

zuban32 updated this revision to Diff 512627.Apr 11 2023, 5:26 PM

Address the comment, add unit test

zuban32 marked an inline comment as done.Apr 11 2023, 5:27 PM
zuban32 updated this revision to Diff 512630.Apr 11 2023, 5:36 PM

Correct merge base

@bzcheeseman pls take a look if the unit test looks good to you. If so I'll go ahead and merge the change, the buildbot failure seems to be unrelated and fixed by revert commit e08af170736271f022b2cab44d58326356ce1db8

The test looks fine to me, but I'm now realizing some tricky behavior. If you std::move a unique pointer into a dyn_cast, and casting fails, you now have no pointer anymore (it's been moved already). In the unit test, if BP couldn't be cast to foo I believe you'd see this behavior. I vaguely recall hitting some of this in the past, does it make sense to have UniquePtrCast take unique_ptr<> & rather than unique_ptr<> && and use .release()/.reset()?

The test looks fine to me, but I'm now realizing some tricky behavior. If you std::move a unique pointer into a dyn_cast, and casting fails, you now have no pointer anymore (it's been moved already). In the unit test, if BP couldn't be cast to foo I believe you'd see this behavior. I vaguely recall hitting some of this in the past, does it make sense to have UniquePtrCast take unique_ptr<> & rather than unique_ptr<> && and use .release()/.reset()?

Oh, you're right, haven't tried that yet but I'm pretty sure that's what's going to happen. I think we could just make dyn_cast take lvalue reference to unique_ptr instead of an rvalue one, then check it with isa<>, and then either move the pointer further or just return nullptr leaving the original pointer intact. I'll update the patch

Oh, you're right, haven't tried that yet but I'm pretty sure that's what's going to happen. I think we could just make dyn_cast take lvalue reference to unique_ptr instead of an rvalue one, then check it with isa<>, and then either move the pointer further or just return nullptr leaving the original pointer intact. I'll update the patch

Thanks! I think we want dyn_cast to consume its input *if it works* and leave it alone if not. Would you mind also dropping a comment to this effect, please?

zuban32 updated this revision to Diff 514531.Apr 17 2023, 11:47 PM
  • Add failed cast test
zuban32 added a comment.EditedApr 17 2023, 11:50 PM

@bzcheeseman I've updated the test and it passes, i.e. I wasn't able to reproduce the behavior we discussed. It seems modern c++ compilers are able to elide the calls to move constructor in such a code, I tried with gcc 9.4 and Apple aarch64 clang-14 with no success.

@bzcheeseman I've updated the test and it passes, i.e. I wasn't able to reproduce the behavior we discussed. It seems modern c++ compilers are able to elide the calls to move constructor in such a code, I tried with gcc 9.4 and Apple aarch64 clang-14 with no success.

This makes me a bit nervous because it feels like undefined behavior unless there's some part of the standard that allows something like this. Do these tests run under ASan/UBSan? Also, I assume you don't get any warnings about use-after-move?

bzcheeseman accepted this revision.May 11 2023, 9:48 AM

@zuban32 Not sure if you were waiting on me (if so, sorry!) - I put some comments inline that would help with the concerns we talked about previously. I'd love to get this landed once the comments are resolved :)

llvm/unittests/Support/Casting.cpp
231

Would it make sense to check something like

base *BP2Ptr = BP2.get();
EXPECT_NE(BP2Ptr, nullptr);
auto DP = dyn_cast<derived_nocast>(std::move(BP2));
EXPECT_EQ(DP.get(), nullptr);
EXPECT_NE(BP2.get(), nullptr);
EXPECT_EQ(BP2.get(), BP2Ptr);

?

That would assuage my concern, I think, as this at least ensures we have the exact behavior we expect on failure.

232

I don't believe this is necessary? I think we want it to be deallocated.

@zuban32 Not sure if you were waiting on me (if so, sorry!) - I put some comments inline that would help with the concerns we talked about previously. I'd love to get this landed once the comments are resolved :)

Absolutely not, I would surely ping you it was about that :) I'm sorry, this got retracted by other things. I was planning to run it with the sanitizer to see if something shows up.

zuban32 added inline comments.May 12 2023, 8:08 PM
llvm/unittests/Support/Casting.cpp
232

I was just following the chunk above, I think gtest reports some memory issue should we omit the release(). I don't remember why, let me take a look

<snip>
Absolutely not, I would surely ping you it was about that :) I'm sorry, this got retracted by other things. I was planning to run it with the sanitizer to see if something shows up.

Alright great - I also got busy so I wasn't sure if I missed something! Definitely feel free to ping me if needed.

bzcheeseman added inline comments.May 12 2023, 8:52 PM
llvm/unittests/Support/Casting.cpp
232

Ah interesting. Thanks for looking into it!

zuban32 updated this revision to Diff 542299.Jul 19 2023, 9:32 PM
  • Make it reliable for unsuccessful casting cases, fix the test
zuban32 added inline comments.Jul 19 2023, 9:37 PM
llvm/unittests/Support/Casting.cpp
231

I've changed the behavior to do the moving only after the casting is proved possible, I haven't noticed any changes in the actual behavior though, both the test above and the actual usecase I needed this for.

232

@bzcheeseman sorry for abandoning this for a while, just made the changes we discussed. release() above was needed because asan couldn't understand where the original pointer came from, after allocating it locally the ptrs are deleted just alright.

bzcheeseman accepted this revision.Aug 11 2023, 9:29 AM

Thank you for iterating! LGTM

This revision was landed with ongoing or failed builds.Aug 13 2023, 6:05 PM
This revision was automatically updated to reflect the committed changes.