-
-
Notifications
You must be signed in to change notification settings - Fork 22.5k
Add RequiredPtr<T>
to mark Object *
arguments and return values as required
#86079
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
base: master
Are you sure you want to change the base?
Conversation
8c39e62
to
f86e10a
Compare
One option would be to have this requirement be strict and crash on failure to fulfil it, I feel this is the area where that'd be appropriate, as if it's not so strict an error print should be used At least I think such an option for this decoration feels appropriate, if not for the complete case |
f86e10a
to
26443b8
Compare
Actually, I think I may be wrong about this part! With gcc/clang there are the EDIT: Heh, and we're even using them in Godot for the crash handler already. :-) |
Very cool, and that was fast! 😎
Could these metadata flags potentially overlap (maybe not now with the ints, but as we add more over time)?
I'm probably missing something here, but if the expected usage is to always abort the function (i.e. print error + return null) after a "required" precondition fails, do we actually need to create a default instance? Not too worried about performance in the error case, but some custom classes might have side effects in their constructors? (Edit: since we're talking about the engine API here, there are probably no custom classes involved; side effects might still apply to some Godot classes though.) |
I really have no idea what metadata could be added in the future. But like I said in the description, something more extensible like the the options you described in godotengine/godot-proposals#2550 would probably be better.
Nope, you're not missing anything, we don't need to create a default instance. I added that because I was worried that engine contributors could mistakenly believe that I'm really not sure what the right behavior is, so that's what I'm hoping to discuss here :-) |
I'd say it depends on how we interpret the argument limitation, is it shouldn't or can't be null? From the proposal:
One use for this would be to remove checks, at least in non-debug builds, the annotation states that "the responsibility of ensuring this is non-null is on the caller" and no checks needs to be done on the callee side In the same sense the check can be used before calling any extension methods or before calling anything from an extension, and exiting there, acting as a barrier I'd prefer it to be a guarantee for the method implementer, a "I can safely skip doing any null checks in this method, someone else handles it for me", or "this can safely be optimized in some sense", akin to the LLVM I'm also not familiar with what the other languages here do for this, so matching some norm there would make sense |
It's hard to plan ahead here. Most flexible would probably be a dictionary (JSON object), but it might also be overkill? {
// ...
"metadata": {
"exact_type": "float64",
"angle": "radian",
"nullable": true
}
} Also, we will need to keep the What I'm a bit worried is that (in a few years), this might devolve into: {
// ...
"meta": "float64|angle=radian|nullable"
} That is, we have a "protocol inside a protocol" -- which would also be breaking btw, since existing code likely checks for the entire value string and would not expect any separators. In other words, we have multiple options:
I tend to favor 1) or 2), but maybe you have more thoughts on this? |
There are many examples of languages that didn't have a concept of nullability/optionality and retrofitted it late. Some I know:
|
An idea I mentioned during today's GDExtension meeting is the type-state pattern, meaning that multiple types are used to represent different states of an object. Concretely, we could split
The core idea here is that you cannot obtain |
f9d00d3
to
0cd5ba3
Compare
@Bromeon I finally got around to reworking this PR based on your suggestion of using the "type-state pattern" from several months ago This requires using new error macros to unpack the underlying pointer, which I think would also lead to encouraging more consistent use of the error macros for pointer parameters, which I think is something @AThousandShips would like per her earlier comments. Please let me know what you think! |
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.
Thanks so much for this! ❤️
Added some first feedback.
It's also convenient that required
meta is mutually exclusive with other metas, since it's an object and others are numbers 🙂
0cd5ba3
to
928b024
Compare
821baf1
to
ae96d1b
Compare
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.
A fantastic foundation for this paradigm, but one that still needs some refinement. You've already mentioned the lack of Ref<T>
support, but there's still a few tricky edge-cases to be considered
core/error/error_macros.h
Outdated
#define ERR_FAIL_REQUIRED_PTR(m_name, m_param) \ | ||
std::remove_reference_t<decltype(m_param)>::type m_name = m_param._internal_ptr(); \ | ||
if (unlikely(m_name == nullptr)) { \ | ||
_err_print_error(FUNCTION_STR, __FILE__, __LINE__, "Parameter \"" _STR(m_param) "\" is null."); \ | ||
return; \ | ||
} else \ | ||
((void)0) |
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.
While this is clever, it goes against the self-contained paradigm of the other macros. That is: this isn't fully wrapped, thanks to the first line needing to be out of scope. If the variable was declared beforehand than this could be fully wrapped, but that would probably end up bloating the code. I don't know if a better alternative exists, but it's something to keep in mind
@@ -1644,11 +1644,11 @@ void Node::_add_child_nocheck(Node *p_child, const StringName &p_name, InternalM | |||
emit_signal(SNAME("child_order_changed")); | |||
} | |||
|
|||
void Node::add_child(Node *p_child, bool p_force_readable_name, InternalMode p_internal) { | |||
void Node::add_child(const RequiredPtr<Node> &rp_child, bool p_force_readable_name, InternalMode p_internal) { |
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.
Not a fan of this naming scheme. It does a great job of reducing code changes, but I think this sort of change should be disruptive. That, and it would mean making an internal variable use the p_*
syntax, which we really shouldn't be encouraging.
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.
As far as I understood, the idea here was to minimize the risk of causing any unintended changes. It's much easier to review just one line of code and have the confidence that nothing breaks.
Maybe the rp_
-> p_
change could still be done in follow-up PRs? These PRs would then become "pure" refactorings and would be completely isolated from semantic changes, which could make it easier to pinpoint the issue in case of regressions.
core/variant/required_ptr.h
Outdated
_FORCE_INLINE_ RequiredPtr(T *p_value) { | ||
value = p_value; | ||
} |
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.
It'd be nice if there was a way to print an error on null assignment, as that would be much clearer to users/maintainers when an improper value is passed. That could be done with a wrapper macro of sorts for construction, but a more convenient option will be added in C++20 via source_location
. Maybe that's outside the scope of this implementation, but I felt it worth mentioning all the same
…s required Co-authored-by: Thaddeus Crews <[email protected]>
This PR is a draft for discussion around proposal godotengine/godot-proposals#2241
What this PR does:
Object *
in method arguments or return values, you can useconst RequiredPtr<Object> &
, which will get the parameter marked as "required" inClassDB
.METADATA_OBJECT_IS_REQUIRED
in the same metadata field that holds things likeMETADATA_INT_IS_UINT32
, which means it'll end up in theextension_api.json
like this, for example:Node::add_child()
as required, although, that's probably not where we'd start with this.ERR_FAILED_REQUIRED_PTR(m_name, m_param)
which acts just likeERR_FAILED_NULL(m_param)
, except it also assigns a local variable with the object pointer if it wasn't null. So, inNode::add_child(const RequiredPtr<Node> &p_child)
it doesERR_FAILED_REQUIRED_PTR(child, p_child)
which will assign aNode *child
variable with the pointer that was inside ofp_child
.RequiredPtr<T>
to be optimized away, and generate basically the same code as using a plainObject *
.UPDATE (2024-12-10): The original version of this had this whole auto-vivifying thing that was terrible, and was replaced with an excellent suggestion from @Bromeon in this comment.