You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In C++, there is often a desire to make a non-virtual call to a virtual function -- for example when implementing a function in a derived class, it's sometimes desirable to call the base class implementation of the function.
base class Base {
virtual fn F[self: Self]() { ... }
}
... and p: Base*, we know:
p->F() performs virtual dispatch
p->F() means the same thing as p->(Base.F)()
Therefore in general, qualified and unqualified calls to virtual functions always perform virtual dispatch -- they're calls to the same callable object Base.F, and its implementation of BindToRef is (presumably) where virtual dispatch happens.
So it seems that it is not straightforward for us to follow C++ and say that qualified calls don't do virtual dispatch, unless we make some kind of hole in our model for member binding operators or qualified name lookup that allows us to distinguish these cases. And it's not even clear that we'd want to: allowing virtual function calls to be forwarded and invoked indirectly is necessary for us to have a story compatible with C++'s story for pointers to members.
Some options:
Do nothing. Provide no mechanism to make a non-virtual call to a virtual function. This means that a virtual function can never be called on the "wrong" dynamic type, and base classes that wish to expose an implementation for a derived class to reuse would need to give that implementation a different name.
This is probably the cleanest and most principled design, and provides the strongest guarantees to class authors.
Major downsides: interoperability with C++ classes may require non-virtual calls to C++ virtual functions, Carbon virtual functions would still be callable with the "wrong" dynamic type from C++, and migration of such calls is more challenging due to requiring a larger-scale rewrite.
Add some mechanism to form a method object that performs non-virtual dispatch. Presumably this would be an operation that takes Base.F and a class derived from Base as input, and returns a callable. There are many options here; we could do this by overloading BindTo* (for example p->(Base.F.(Base))()) or adding a member function to the type of virtual functions (p->(Base.F.NonVirtual(Base))()) or adding a member function to the type of bound virtual functions (p->F.NonVirtual(Base)() -- though this would presumably first perform a vtable lookup then throw away the result, which seems suboptimal) or adding a free function (p->(NonVirtual(Base.F, Base))()) or ...
This option seems like it might be a reasonable approach, with the right syntax.
Use different (non-method) call notation for a non-virtual call. For example, we could permit Base.F(p) as a direct non-virtual call, and p->(Base.F)() as a virtual call.
Distinguish qualified and unqualified call syntax, perhaps with additional interfaces around BindToRef that distinguish direct and indirect binding in some way. Eg, p->F could mean something slightly different from p->(Base.F) or p->(f), with the former performing virtual dispatch and the latter not doing so.
This would probably be the most similar to C++, but harms our story for providing a feature similar to pointers-to-members.
Distinguish specifically the case of p->(Class.F)(), and say that performs a non-virtual call, whereas let f: auto = Class.F and p->(f)() would perform a virtual call.
This would be a non-uniformity in the language behavior.
Does not provide a direct way to form a non-virtual function value that can be passed to another function. This can perhaps be emulated with a lambda. Note that C++ pointers-to-members don't support this either.
A related question is whether Derived.F is a different value from Base.F, or just finds F in the extended base class. The impl fn F() doesn't need to introduce a new, shadowing F, just to implement the existing F, if there is no situation where Derived.F behaves differently from Base.F.
Any other information that you want to share?
A related concern is the behavior of derived_p->base.F(). Intuitively it seems like this might invoke the base class's version of F, but presumably won't: derived_p->base is a reference expression naming a Base object, so derived_p->base should behave like *base_p, and (*base_p).F() should perform virtual dispatch, and so derived_p->base.F() seems like it must also perform virtual dispatch, unless we add some kind of special case.
The text was updated successfully, but these errors were encountered:
The repetitiveness of p->(Base.F.NonVirtual(Base))() is a bit of a concern for me. There are two different Bases here with two different meanings -- one is where we look up F and the other is the derived type whose implementation we're using (and there's the secret Base we get from the type of p). Reducing this a bit would be nice if we take this route -- perhaps something like p->(NonVirtual(Base).F)() could work?
Summary of issue:
In C++, there is often a desire to make a non-virtual call to a virtual function -- for example when implementing a function in a derived class, it's sometimes desirable to call the base class implementation of the function.
How do we express this kind of pattern in Carbon?
Details:
In Carbon, given
base class Base { virtual fn F[self: Self]() { ... } }
... and
p: Base*
, we know:p->F()
performs virtual dispatchp->F()
means the same thing asp->(Base.F)()
Therefore in general, qualified and unqualified calls to virtual functions always perform virtual dispatch -- they're calls to the same callable object
Base.F
, and its implementation ofBindToRef
is (presumably) where virtual dispatch happens.So it seems that it is not straightforward for us to follow C++ and say that qualified calls don't do virtual dispatch, unless we make some kind of hole in our model for member binding operators or qualified name lookup that allows us to distinguish these cases. And it's not even clear that we'd want to: allowing virtual function calls to be forwarded and invoked indirectly is necessary for us to have a story compatible with C++'s story for pointers to members.
Some options:
Base.F
and a class derived fromBase
as input, and returns a callable. There are many options here; we could do this by overloadingBindTo*
(for examplep->(Base.F.(Base))()
) or adding a member function to the type of virtual functions (p->(Base.F.NonVirtual(Base))()
) or adding a member function to the type of bound virtual functions (p->F.NonVirtual(Base)()
-- though this would presumably first perform a vtable lookup then throw away the result, which seems suboptimal) or adding a free function (p->(NonVirtual(Base.F, Base))()
) or ...Base.F(p)
as a direct non-virtual call, andp->(Base.F)()
as a virtual call.BindToRef
that distinguish direct and indirect binding in some way. Eg,p->F
could mean something slightly different fromp->(Base.F)
orp->(f)
, with the former performing virtual dispatch and the latter not doing so.p->(Class.F)()
, and say that performs a non-virtual call, whereaslet f: auto = Class.F
andp->(f)()
would perform a virtual call.A related question is whether
Derived.F
is a different value fromBase.F
, or just findsF
in the extended base class. Theimpl fn F()
doesn't need to introduce a new, shadowingF
, just to implement the existingF
, if there is no situation whereDerived.F
behaves differently fromBase.F
.Any other information that you want to share?
A related concern is the behavior of
derived_p->base.F()
. Intuitively it seems like this might invoke the base class's version ofF
, but presumably won't:derived_p->base
is a reference expression naming aBase
object, soderived_p->base
should behave like*base_p
, and(*base_p).F()
should perform virtual dispatch, and soderived_p->base.F()
seems like it must also perform virtual dispatch, unless we add some kind of special case.The text was updated successfully, but these errors were encountered: