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

Improve manual implementations of up- and downcasting #1204

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

jnbooth
Copy link
Contributor

@jnbooth jnbooth commented Mar 1, 2025

This PR provides some minor improvements to #1159 for the implementations of Upcast.

  • Replaces downcastPtr's use of dynamic_cast with qobject_cast, which is much faster and does not require RTTI.
  • Implements Upcast<QVector<QPoint>> for QPolygon, replacing the standalone functions previously used for Deref and DerefMut.
  • Implements Upcast<QVector<QPointF>> for QPolygonF, replacing the standalone functions previously used for Deref and DerefMut.
  • Implements Upcast<QCoreApplication> for QGuiApplication and adds Deref. ¹
  • Implements Upcast<QGuiApplication> for QApplication and adds Deref. ¹
  • Implements Deref for QQmlApplicationEngine to QQmlEngine using the existing upcasting implementation. ¹
  • Removes a null pointer created inside QStringList's implementation of Upcast<QList<QString>> due to a misuse of dynamic_cast.

¹ Removed in favor of #1202.

Copy link

codecov bot commented Mar 1, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 100.00%. Comparing base (64d4ee6) to head (5075d50).

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #1204   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           73        73           
  Lines        12612     12612           
=========================================
  Hits         12612     12612           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jnbooth jnbooth force-pushed the non-qobject-casting branch 2 times, most recently from d144680 to 6d63031 Compare March 1, 2025 20:42
@jnbooth jnbooth force-pushed the non-qobject-casting branch 2 times, most recently from eeab59b to dee86e9 Compare March 1, 2025 22:07
@jnbooth jnbooth force-pushed the non-qobject-casting branch from dee86e9 to 00886c0 Compare March 1, 2025 22:10
@jnbooth jnbooth force-pushed the non-qobject-casting branch from 00886c0 to 311ddd9 Compare March 1, 2025 22:56
@@ -22,7 +23,15 @@ const Sub*
downcastPtr(const Base* base)
{
static_assert(std::is_base_of_v<Base, Sub>);
return dynamic_cast<const Sub*>(base);
return qobject_cast<const Sub*>(base);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure we want to use qobject_cast ? As this then limits to only based QObject classes, but there are classes in Qt we need to supportt that do not have QObject as a base ?

Or we are going to need some logic in the generation of the automatic up/down casts to figure out whether it should use a qobject cast or not (maybe if there is a #[qobject] attribute or not).

Copy link
Contributor Author

@jnbooth jnbooth Mar 3, 2025

Choose a reason for hiding this comment

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

What we shouldn't use is dynamic_cast. There are only a few cases of Qt class inheritance that isn't based on a QObject, and those should use static_cast, as with QPolygon and QStringList. This is due to the complicated way that C++ handles RTTI and polymorphism. dynamic_cast only works if the class has at least one virtual method so it can check RTTI (and it has some other esoteric limitations that make it a poor tool for almost any task). For Qt, that means it wouldn't work unless it's a QObject anyway. That's why downcasting QStringList didn't work using dynamic_cast.

Given the #[base] pragma being added by #1202, I think we would probably want to break it down like this:

  • If the class inherits QObject, use #[base], which internally uses qobject_cast. This can be done both internally and by end users. Probably don't automatically implement Deref as it'd be too complicated. Plus, QObjects can't implement DerefMut because their mutable methods use Pin<&mut Self>.
  • Otherwise, use static_upcast from those extern utility functions and implement Deref and DerefMut. (DerefMut can be implemented because their mutable methods use &mut self.) This should only be done internally, because otherwise it might get added to something that isn't actually valid to cast.

Copy link
Collaborator

@LeonMatthesKDAB LeonMatthesKDAB Mar 5, 2025

Choose a reason for hiding this comment

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

As we're talking about downcasting, static_cast is a no-go in the general case, we do not know if the type is actually of the subclass, so we need to check at runtime. static_cast would do the cast in any case, which is instant undefined behavior if the type is not actually of the subclass.

dynamic_cast is the only option we have for non-QObject types in that case, even given it's limitations.

@jnbooth you bring up some good points though, which we should integrate.

  1. We can use is_polymorphic to assert that a type actually has at least one virtual function so we can use dynamic_cast.
  2. We can use template meta-programming with is_base_of to detect if a type is derived from QObject and then use qobject_cast instead of dynamic_cast.

That way, we can still have a single templated method that uses the optimal cast in all cases.
@jnbooth If you want I can send you the required code, in case you don't want to deal with the template meta programming. Just let me know.

For types that don't meet these criteria, we likely don't want do support downcast?
I guess we could just always return nullptr there, which would turn into None in the Rust impl.

@ahayzen-kdab may need to respect this in #1202 . 🤔

Copy link
Contributor Author

@jnbooth jnbooth Mar 6, 2025

Choose a reason for hiding this comment

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

To be clear, I'm not suggesting static_cast should be used in the general case or any generated code. I am only using it here, manually implemented, for cases where static_cast is always safe, per its safety requirements. downcastPtrStatic is never used anywhere else, nor should it be.

Are there any examples you have of types that might need to be downcasted that aren't QObjects, aside from the above special cases of classes that are always safe to static_cast (there are only a few of them)? I think it is probably a better idea to handle those few cases internally and use qobject_cast for everything else, because users can only use QObjects anyway. dynamic_cast is generally not a good idea, to the point that it is sometimes labeled "evil".

Copy link
Collaborator

Choose a reason for hiding this comment

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

For other examples, you might have a class that is a gadget or something that is just a raw class in Qt like an implementation of QQuickFramebufferObject::Renderer. You may then take the base class as a parameter and want to downcast potentially to your class.

I agree where we have #[qobject] on the class we should be able to determine that we can use qobject_cast.

So i think we then potentially end up with the following

  • static_cast when safe to do so (eg type is of same size and derived from the other etc)
  • qobject_cast when QObject is a base class of the object
  • dynamic_cast for anything else that does not have a QObject base or does not have same size or is not derived from the other etc

@LeonMatthesKDAB
Copy link
Collaborator

@jnbooth Thank you for the contribution, this is looking good.

Regarding the downcasting, I think we can get all those concerns implemented in a sane way with a bit of meta-programming that automatically selects between qobject_cast and dynamic_cast.

And keeping static_cast for the special cases is okay, though I think we should add another assertion just out of caution.

@jnbooth
Copy link
Contributor Author

jnbooth commented Mar 29, 2025

@LeonMatthesKDAB I don't know how to do that kind of metaprogramming. Are there examples of it in the codebase that I could learn from? Alternatively, should I strip down part of this PR so it can be merged without that?

jnbooth and others added 2 commits March 31, 2025 11:43
Add detection whether to use qobject_cast or dynamic_cast via template
specialization.

Also add additional checks that a static_cast downcast is sane to do
(i.e. at least the size of the types must be the same).
@LeonMatthesKDAB
Copy link
Collaborator

@jnbooth This should do it:
LeonMatthesKDAB@fdd65e9
In case you're interested, google SFINAE and enable_if_t, it's one of the crazier C++ concepts for sure.
But is quite powerful for cases like this.

I pushed this to a branch on my fork (non-qobject-casting) and rebased, feel free to cherry-pick or just use that branch for your PR.

My template meta-programming is a bit rusty nowadays, so @ahayzen-kdab please review carefully to check this is sane.

@jnbooth jnbooth requested a review from ahayzen-kdab April 8, 2025 17:55
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.

3 participants