-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Extensible classes with or without vtables #652
Comments
Going through the process of writing this up has moved me firmly into the "only deallocate final types" camp. I like the simplicity of the rule, how easy it is to understand why the rule makes the code safe, how you can bypass it when needed while clearly marking that what you are doing is unsafe, and how well it handles the assignment issue at the same time. I feel like the last two solutions are too much mechanism for an advanced use case that we don't really have a reason to encourage more strongly. If it did seem burdensome, I'd consider the "automatic final second type" option as something easy to add later, but I don't like the long names that result. I think the main reason to support another solution would be if that solution also addressed the assignment operator problem for types with virtual destructors. For example, maybe all non-final types that can be instantiated will have a |
I think I like the model that you can only allocate and deallocate types that are either final or have virtual destructors (or some similar mechanism that hooks into deallocation and that we trust to do the right thing). I think this should only extend to heap allocation and deallocation, though, and not to construction and destruction in general. For example, for a case like:
... I think it would be surprising if we rejected the code, but that adding a virtual destructor to
(or however we spell heap allocation and deallocation). |
I've written a doc exploring options for the more general problem of what to do with extensible classes, whether they have virtual tables or not. |
The open discussion seems to have been converging on following C++ for the most part:
For now, we are going to avoid having a shadow type system to track which values are exactly the declared type, rather than some derived type. |
It seems we have consensus as summarized here: #652 (comment) Everyone seems to largely agree that only having abstract and final classes provides a more clean and error resistant model. However, the ergonomics are significantly regressed with that model, especially compared to C++. It doesn't seem feasible to ask people to pay the overhead of that model -- both the direct cost is high and the errors prevented aren't bad or pervasive enough to warrant the cost. The other alternatives seemed complex, inventive, or had unpleasant implications. None really were compelling compared to matching what C++ does. So the result was to stay very close to C++, and to take a somewhat tactical approach to avoiding the pitfalls that do arise here such as restricting the deletion of an object through a pointer. If more of the rationale is needed, feel free to poke folks to get them to add it, but let's consider this resolved. |
(To be clear, I believe the rationale I suggest here matches what is in #777 already. Happy to adjust / clarify if anyone spots an important difference here.) |
Note: answering this question is not urgent, but I wanted to capture the options while they were fresh in our minds
The use case in question is that we have type
Derived
extendingBase
, andBase
does not have a virtual destructor. We would like to statically verify that the user's deallocation code is safe (with the possibility of an unsafe opt-out). We have a few approaches:Restricted extension
We could say that the destructor defined in
Base
must be a legal destructor forDerived
. Since we have use cases whereDerived
adds fields toBase
(for exampleBase
is "linked list node just the pointers used for sentinels" andDerived
adds data members), non-virtual non-final deletes would have to be unsized. No fields added inDerived
would be allowed to have non-trivial destructors, andDerived
would not be allowed to define its own destructor. This follows the "you can't override what wasn't declared virtual" rule.Only deallocate final types
The rule would be that you would not be allowed to allocate or deallocate non-final types with non-virtual destructors. Users would define another type (call it
Concrete
) derived fromBase
that would be declaredfinal
and actually instantiated, used for locals and containers, etc.Base
would only be used for pointers that could point to values that could either beConcrete
orDerived
and deleting aBase*
would be forbidden. In general this would be safe, except when users perform a downcast fromBase*
which is generally already understood to be an unsafe operation.My expectation is that there would be some convention for naming the final class as
Foo
and the non-final one asFooBase
. This makes variables and containers use the shorter name, and we keep the longer name for pointers where we are being explicit that it would be pointing to another type.This option is tempting since:
Automatic second type for pointers
The rule would be:
Base
would automatically have a type member namedOrChild
(or some similar name)Derived*
toBase*
, only toBase.OrChild*
Base.OrChild
objectIn non-pointer contexts, you would use
Base
and it would do what is expected in context. Unlike the previous option, you would be able to defineDerived
as extendingBase
even though you could instantiate values of typeBase
.This option is not as clean as the previous case, since
Base
would be treated asfinal
in some contexts but not others. This is evident in a less clear story for handing assignment. However it would save the boilerplate of manually defining an extra type, which might be important if this is a common use case.Automatic final second type
The rule would be:
Base
that could be instantiated would automatically have a type member namedFinal
(or some similar name)Base.Final
would extendBase
without changing anything except making itfinal
Base.Final
notBase
would be allowed to be allocated or deletedThe downside here is that you would frequently be using the name
Base.Final
and it seems like it might be verbose and surprising. The good news is that I could imagine implementing the assignment operator only onBase.Final
using an external interface implementation.Two kinds of pointers
The sad part of the previous three options is that we would have two separate types that are only really different when using pointers. The concern is that they would cause a proliferation of types, leading to additional monomorphization with generics, etc. We could instead just have two kinds of pointer types: "pointer to exactly
Base
" and "pointer toBase
or derived". You would not be able to deallocate through the latter type.Advantage:
Disadvantage:
The text was updated successfully, but these errors were encountered: