carbon-lang
carbon-lang copied to clipboard
should we support deduction from base classes?
Summary of issue:
Should Carbon's argument deduction be able to deduce from a base class of the type of the value we are matching against?
Details:
When deducing the arguments in a function call, C++ permits a parameter that is a pointer to a base class template specialization to have its arguments deduced from a pointer to a derived class:
template<typename T> class Base {};
class Derived : Base<int> {};
template<typename T> void f(Base<T>*);
// OK: deduces T = int.
void g(Derived d) { f(&d); }
The analogous case in Carbon would be something like:
base class B(T:! Type) {}
class D extends B(i32) {}
fn F[T:! Type](p: B(T)*) {}
fn Main() {
var d: D = {};
// Is this OK, deducing T = i32, or an error?
F(&d);
}
We should decide whether we perform such deductions.
Any other information that you want to share?
In C++, there is additional complexity due to multiple inheritance, where multiple different sibling base classes might all match the given deduction pattern. In Carbon, we can say that we only match against the derived-most class that matches, or against only the derived-most instance of the same generic class (even if it doesn't match and derives from one that does).
I think this feature would be a valuable part of the "derived classes may be substituted where their base classes are expected" which is part of supporting the Liskov Substitution Principle. When looking at extensible classes, I noticed that C++ often seems to favor substitutability of derived classes over safety. We have so far in Carbon mostly stayed consistent with that choice by not distinguishing "exactly Base" from "Base or Derived" in the type system. I don't think this particular feature causes any additional trouble beyond what is introduced by allowing the conversion from D* to B* when the base class is not parameterized.
I hate to disagree but I don't think it would be valuable.
I hate to disagree but I don't think it would be valuable.
Just disagreeing doesn't add much value to the conversation. Could you provide reasoning and justification for that position?
Right now I see supporting this feature as a benefit of only supporting single inheritance.
I can provide a few arguments against this:
-
This would be the sole exception to a general principle, that argument deduction attempts to find values for deduced parameters that make the substituted type match the argument type. (It is the only "surprising" exception to that principle in C++, but not the only exception; there are other exceptions because
constandvolatilequalifiers, andnoexcepton a function type, are also allowed to differ, and because of reference types. See https://eel.is/c++draft/temp.deduct.call#4) -
This adds complexity to the overall deduction process, because this is the only form of deduction where we would speculatively try deducing something, and potentially need to backtrack and try a different deduction. (This has been a source of bugs across C++ compilers.)
-
If we support this by itself, there would be no mechanism to get comparable behavior for similar situations where implicit conversion is used rather than inheritance. (For example, I can write a function that takes
Int(N)for anyN, and call it with anInt(N)or a class derived fromInt(N), but I wouldn't be able to call it with a class that implicitly converts toInt(N).)
And a couple of arguments in favor:
-
This allows objects of derived types to be passed to functions expecting the base class, even when the base class is parameterized.
-
C++ allows this, so supporting it will improve our ability to model the same kinds of function behavior that C++ does.
This seems related to something recently fixed in Swift: https://forums.swift.org/t/recent-improvements-to-associated-type-inference/70265#superclass-constrained-associated-types-3