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

how to make a non-virtual call to a virtual function? #4109

Open
zygoloid opened this issue Jul 3, 2024 · 1 comment
Open

how to make a non-virtual call to a virtual function? #4109

zygoloid opened this issue Jul 3, 2024 · 1 comment
Labels
leads question A question for the leads team

Comments

@zygoloid
Copy link
Contributor

zygoloid commented Jul 3, 2024

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.

class Base {
public:
  virtual void f() { ... }
};
class Derived : public Base {
public:
  virtual void f() override {
    // ... Derived things
    Base::f();
    // ... Derived things
  }
};

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 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.

@zygoloid zygoloid added the leads question A question for the leads team label Jul 3, 2024
@zygoloid
Copy link
Contributor Author

zygoloid commented Jul 3, 2024

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
leads question A question for the leads team
Projects
None yet
Development

No branches or pull requests

1 participant