Description
Box::downcast is built on an Any trait that requires compiler support and can't be reproduced directly in C++.
Downcasting correctly is a big part of memory safety/unsafety in C++ though.
Blink uses Downcast traits that you can specialize for your type to say if a particular object can be downcasted to a type.
// Helpers for downcasting in a class hierarchy.
//
// IsA<T>(x): returns true if |x| can be safely downcast to T*. Usage of this
// should not be common; if it is paired with a call to To<T>, consider
// using DynamicTo<T> instead (see below). Note that this also returns
// false if |x| is nullptr.
//
// To<T>(x): unconditionally downcasts and returns |x| as a T*. CHECKs if the
// downcast is unsafe. Use when IsA<T>(x) is known to be true due to
// external invariants and not on a performance sensitive path.
// If |x| is nullptr, returns nullptr.
//
// DynamicTo<T>(x): downcasts and returns |x| as a T* iff IsA<T>(x) is true,
// and nullptr otherwise. This is useful for combining a conditional
// branch on IsA<T>(x) and an invocation of To<T>(x), e.g.:
// if (IsA<DerivedClass>(x))
// To<DerivedClass>(x)->...
// can be written:
// if (auto* derived = DynamicTo<DerivedClass>(x))
// derived->...;
//
// UnsafeTo<T>(x): unconditionally downcasts and returns |x| as a T*. DCHECKs
// if the downcast is unsafe. Use when IsA<T>(x) is known to be true due
// to external invariants. Prefer To<T> over this method, but this is ok
// to use in performance sensitive code. If |x| is nullptr, returns
// nullptr.
//
// Marking downcasts as safe is done by specializing the DowncastTraits
// template:
//
// template <>
// struct DowncastTraits<DerivedClass> {
// static bool AllowFrom(const BaseClass& b) {
// return b.IsDerivedClass();
// }
// static bool AllowFrom(const AnotherBaseClass& b) {
// return b.type() == AnotherBaseClass::kDerivedClassTag;
// }
// };
//
// int main() {
// BaseClass* base = CreateDerived();
// AnotherBaseClass* another_base = CreateDerived();
// UnrelatedClass* unrelated = CreateUnrelated();
//
// std::cout << std::boolalpha;
// std::cout << IsA<Derived>(base) << '\n'; // prints true
// std::cout << IsA<Derived>(another_base) << '\n'; // prints true
// std::cout << IsA<Derived>(unrelated) << '\n'; // prints false
// }
template <typename T>
struct DowncastTraits {
template <typename U>
static bool AllowFrom(const U&) {
static_assert(sizeof(U) == 0, "no downcast traits specialization for T");
NOTREACHED();
return false;
}
};
And then has IsA
(querying), To
(panicking), DynamicTo
(fallible) and UnsafeTo
(unchecked) functions to do the downcast. This is a lot like llvm's llvm::isa
, llvm::cast
and llvm::dyn_cast
.
The DowncastTraits
are very very much like how we're now implementing most concepts in subspace. If we were to add a concept CanBeDowncasted
which tests for the presence of the downcast traits, it would be basically the exact same, but restricting to a generic "downcastable" type isn't that interseting.
A better concept would be CanBeDowncastTo<From, To>
which checks both for the presence of DowncastTraits
and that To
is a subclass of From
, which you can then write:
void f(CanBeDowncastTo<Frog> auto* frog_supertypes) {}
Which would accept anything Frog
derives from. This is not a super common paradigm to express today. Code works with higher level concepts and then downcasts to the specific stuff dynamically, rather than wanting something generic that can already be known to be maybe that subtype. This generic concept would be useful in the case of multiple inheritance and wanting any of those super types, I'm not sure I can give a good example. When you have a concrete super type instead, you already know it "can be downcasted" except that we would express that through the (very unsafe) static_cast.
So what the concept then provides is a compile time check for the downcast traits on the call signature, but the compiler error when you try to use them would be sufficient, unless you want to work with generics as in "some superclass in a multi-inheritance tree".
tl;dr I don't see the reason to add a concept for Any
that just checks if a type can downcast to something right now, but I would love to come up with an example that would benefit.
What I see in Rust is this used for type erasure instead of inheritance. Since you can't upcast to a super class, you convert to an Any
trait. Inheritance is stronger in that it puts into the type system a restriction on which things you can try downcast to, as it has to be a subtype. But then C++ jumps directly to Undefined Behaviour by making the cast infallible.
Most likely the right answer is to reproduce Blinks DowncastTraits verbatim, I don't know of any issue with them, and continue to lean on inheritance rather than generics for expressing a super type (see also sus::error::DynError
).
So then coming back to Box::downcast
, which is present for Box<T: Any>
and calls through to Any::isa
/Any::downcast_unchecked
. We could provide a Box::downcast
that is present when DowncastTraits
are present. What's the point?
The point is that for unique_ptr to downcast you have to do make_unique_ptr<SubType>(static_cast<Subtype*>(uniq.release()))
which is bad. Leaving the unique_ptr can drop the deleter, it can make static analysis much harder, and it necessitates the API to provide release() and construction from a native pointer, and for developers to be used to them.
So Box<SuperType>::downcast() -> Box<SubType>
is worth having, and that it can be fallible and chain with Result/Option/Iterator is good too. And compile-failure inside downcast() on a missing DowncastTraits is not great. So we're back to the concept being a good idea. Not because users will write generic code on the concept, but because Container APIs may want to provide fallible downcast behaviour that is conditionally available.