Frequently, an expression provided as input to an operation has a type that does not exactly match the expected type. To improve the language ergonomics, we do not want to require explicit conversions in all such cases. However, there is strong evidence from C++ that allowing certain kinds of implicit conversion is dangerous and harmful in practice. We need to find a reasonable balance.
C++ permits many kinds of implicit conversion, some of which are generally considered good, and others are sometimes harmful. For example:
int implicitly converts to long. This is useful and seldom harmful.long implicitly converts to int and to unsigned int. This can result
in data loss.int* implicitly converts to bool. This can be useful in some contexts,
such as if (p), but surprising and harmful in others.See also implicit conversions in C++.
See changes to design.
We could permit more of the conversions that C++ does. This section considers each kind of implicit conversion in C++ and provides a description of the deviation and a rationale.
Array types have not yet been designed yet, so this is out of scope for now.
One possible design would be for pointers to not support arithmetic, and for arrays to provide "array iterators" that do supply such arithmetic. In this design, an implicit conversion from arrays to array iterators would likely be surprising.
Function pointer types have not been designed yet, and might not exist in the same form as in C++, so this is out of scope for now.
One possible design would be to have no function pointer types, and instead
model functions as values of a unique type that implements a certain Callable
interface. Then a function pointer could be modeled as a type-erased generic
implementing Callable. In this model, there would be an implicit conversion
from a function value to such a type-erased generic value.
So far, Carbon has no notion of cv-qualification. However, these conversions
would likely be covered by the permission to convert from T* to U* if T is
a subtype of U.
Carbon disallows implicit conversion from bool to integral types. We could
permit such implicit conversions.
Advantages:
if (cond1 + cond2 + cond3 >= 2).Disadvantages:
bool argument is passed to a parameter of some other type.This conversion is permitted.
These conversions are only permitted when they are known to preserve the original value. These are the conversions that are considered non-narrowing in C++.
We could permit narrowing integer conversions.
Advantages:
char n; char c = '0' + n; where C++ promotes '0' + n to int.
i32 here.Disadvantages:
Carbon disallows implicit conversion from a more-precise floating-point type to
a less-precise floating-point type, such as from f64 to f32. We could permit
these implicit conversions.
Advantages:
float a, b; float c = a + b; where C++ promotes a + b to double.
f64 here.Disadvantages:
Carbon permits the equivalent conversions, except for the conversion from
nullptr to pointer type. We anticipate that Carbon pointers will not be
nullable by default.
Once nullable pointers are designed, we would expect an expression representing the null state would be implicitly convertible to the nullable pointer type.
Carbon does not yet have pointer-to-member types. This is out of scope for now.
Carbon does not yet have function pointer types. This is out of scope for now.
An implicit conversion from arithmetic types and pointer types to bool is not
provided. Pointer types are expected to not be nullable by default, so that part
is out of scope for now.
We could permit implicit conversion from arithmetic types to bool.
Advantages:
Disadvantages:
bool parameter.bool being a choice type rather than an integer
type.bool".We could permit no implicit conversions at all, or restrict the set of conversions from those proposed.
Advantages:
Disadvantages:
We could provide only built-in conversions and no user-defined implicit conversions.
Advantages:
Disadvantages:
We could apply implicit conversions transitively. If an implicit conversion from
A to B is provided and an implicit conversion from B to C is provided,
we could try to infer an implicit conversion from A to C.
This leads to practical problems, as there would be an unbounded search space
for intermediate B types. For example:
impl [T:! Constraint1] A as ImplicitAs(T);
impl [T:! Constraint2] T as ImplicitAs(B);
let x: A = ...;
let y: B = x as B;
There is a potentially unbounded space of types to search here (anything that
satisfies both Constraint1 and Constraint2 at once. Similarly:
class X(N: i32, M: i1) {}
impl [template N:! i32] X(N, 0) as ImplicitAs(X(N+1, 0));
impl [template N:! i32] X(N, 0) as ImplicitAs(X(N+1, 1));
impl [template N:! i32] X(N, 1) as ImplicitAs(X(N+1, 1));
let z: auto = ({} as X(0, 0)) as X(100, 0);
This could lead to a very long implicit conversion sequence (which will presumably need exponential runtime to find).
We could support partial transitivity, for only unparameterized intermediate
types, by ignoring all blanket impls. But that would be arbitrary, and we can
provide better results by first matching the overall source and destination
types and then asking them what intermediate type we should be converting to,
which is supported by this proposal. For example, for Optional:
impl [T:! Type, U:! ImplicitAs(T)] U as ImplicitAs(Optional(T)) {
fn Convert[me: T]() -> Optional(T) { return ...; }
}