Implicit conversions (#820)

Proposal to support a limited set of implicit conversions.

This would generally permit only implicit conversions that are lossless and semantics-preserving. In particular, this proposal allows:

-   Conversion from an integer type to a wider integer type of the same signedness, and from an unsigned integer type to a wider signed integer type.
-   Conversion from an integer type to a floating-point type that has enough mantissa bits to exactly represent all integers in the source type.
-   Conversion from integer literals to integer and floating-point types that can represent them.
-   Conversion from floating-point literals to floating-point types that can represent them.
-   Conversions required for generics: conversions of values between facet types, and conversions of types between type-of-types, as described in the generics proposals.
-   Conversions required for inheritance: derived-to-base conversions for class pointers and class values.

Other conversions, such as lossy conversions between arithmetic types and conversions between bool and other types are not supported.

Co-authored-by: josh11b <josh11b@users.noreply.github.com>
Co-authored-by: Chandler Carruth <chandlerc@gmail.com>
Co-authored-by: Geoff Romer <gromer@google.com>
This commit is contained in:
Richard Smith
2021-09-21 15:16:16 -07:00
committed by GitHub
co-authored by josh11b Chandler Carruth Geoff Romer
parent 7beaabc934
commit f63169608e
6 changed files with 674 additions and 35 deletions
+44
View File
@@ -0,0 +1,44 @@
# Expressions
<!--
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
Exceptions. See /LICENSE for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-->
<!-- toc -->
## Table of contents
- [Overview](#overview)
- [Implicit conversions](#implicit-conversions)
<!-- tocstop -->
## Overview
Expressions are the portions of Carbon syntax that produce values. Because types
in Carbon are values, this includes anywhere that a type is specified.
```
fn Foo(a: i32*) -> i32 {
return *a;
}
```
Here, the parameter type `i32*`, the return type `i32`, and the operand `*a` of
the `return` statement are all expressions.
## Implicit conversions
When an expression appears in a context in which an expression of a specific
type is expected, [implicit conversions](implicit_conversions.md) are applied to
convert the expression to the target type.
```
fn Bar(n: i32);
fn Baz(n: i64) {
// OK, same as Bar(n as i32)
Bar(n);
}
```
@@ -0,0 +1,297 @@
# Implicit conversions
<!--
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
Exceptions. See /LICENSE for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-->
<!-- toc -->
## Table of contents
- [Overview](#overview)
- [Properties of implicit conversions](#properties-of-implicit-conversions)
- [Lossless](#lossless)
- [Semantics-preserving](#semantics-preserving)
- [Examples](#examples)
- [Built-in types](#built-in-types)
- [Data types](#data-types)
- [Equivalent types](#equivalent-types)
- [Pointer conversions](#pointer-conversions)
- [Pointer conversion examples](#pointer-conversion-examples)
- [Type-of-types](#type-of-types)
- [Semantics](#semantics)
- [Extensibility](#extensibility)
- [Alternatives considered](#alternatives-considered)
- [References](#references)
<!-- tocstop -->
## Overview
When an expression appears in a context in which an expression of a specific
type is expected, the expression is implicitly converted to that type if
possible.
For [built-in types](#built-in-types), implicit conversions are permitted when:
- The conversion is [_lossless_](#lossless): every possible value for the
source expression converts to a distinct value in the target type.
- The conversion is [_semantics-preserving_](#semantics-preserving):
corresponding values in the source and destination type have the same
abstract meaning.
These rules aim to ensure that implicit conversions are unsurprising: the value
that is provided as the operand of an operation should match how that operation
interprets the value, because the identity and abstract meaning of the value are
preserved by any implicit conversions that are applied.
It is possible for user-defined types to [extend](#extensibility) the set of
valid implicit conversions. Such extensions are expected to also follow these
rules.
## Properties of implicit conversions
### Lossless
We expect implicit conversion to never lose information: if two values are
distinguishable before the conversion, they should generally be distinguishable
after the conversion. It should be possible to define a conversion in the
opposite direction that restores the original value, but such a conversion is
not expected to be provided in general, and might be computationally expensive.
Because an implicit conversion is converting from a narrower type to a wider
type, implicit conversions do not necessarily preserve static information about
the source value.
### Semantics-preserving
We expect implicit conversions to preserve the meaning of converted values. The
assessment of this criterion will necessarily be subjective, because the
meanings of values generally live in the mind of the programmer rather than in
the program text. However, the semantic interpretation is expected to be
consistent from one conversion to another, so we can provide a test: if multiple
paths of implicit conversions from a type `A` to a type `B` exist, and the same
value of type `A` would convert to different values of type `B` along different
paths, then at least one of those conversions must not be semantics-preserving.
A semantics-preserving conversion does not necessarily preserve the meaning of
particular syntax when applied to the value. The same syntax may map to
different operations in the new type. For example, division may mean different
things in integer and floating-point types, and member access may find different
members in a derived class pointer versus in a base class pointer.
### Examples
Conversion from `i32` to `Vector(int)` by forming a vector of N zeroes is
lossless but not semantics-preserving.
Conversion from `i32` to `f32` by rounding to the nearest representable value is
semantics-preserving but not lossless.
Conversion from `String` to `StringView` is lossless, because we can compute the
`String` value from the `StringView` value, and semantics-preserving because the
string value denoted is the same. Conversion in the other direction may or may
not be semantics-preserving depending on whether we consider the address to be a
salient part of a `StringView`'s value.
## Built-in types
### Data types
The following implicit numeric conversions are available:
- `iN` or `uN` -> `iM` if `M` > `N`
- `uN` -> `uM` if `M` > `N`
- `fN` -> `fM` if `M` > `N`
- `iN` or `uN` -> `fM` if every value of type `iN` or `uN` can be represeted
in `fM`:
- `i12` or `u11` (or smaller) -> `f16`
- `i25` or `u24` (or smaller) -> `f32`
- `i54` or `u53` (or smaller) -> `f64`
- `i65` or `u64` (or smaller) -> `f80` (x86 only)
- `i114` or `u113` (or smaller) -> `f128` (if available)
- `i238` or `u237` (or smaller) -> `f256` (if available)
In each case, the numerical value is the same before and after the conversion.
An integer zero is translated into a floating-point positive zero.
An integer constant can be implicitly converted to any type `iM`, `uM`, or `fM`
in which that value can be exactly represented. A floating-point constant can be
implicitly converted to any type `fM` in which that value is between the least
representable finite value and the greatest representable finite value
(inclusive), and does not fall exactly half-way between two representable
values, and converts to the nearest representable finite value.
The above conversions are also precisely those that C++ considers non-narrowing,
except:
- Carbon also permits integer to floating-point conversions in more cases. The
most important of these is that Carbon permits `i32` to be implicitly
converted to `f64`. Lossy conversions, such as from `i32` to `f32`, are not
permitted.
- What Carbon considers to be an integer constant or floating-point constant
may differ from what C++ considers to be a constant expression.
**Note:** We have not yet decided what will qualify as a constant in this
context, but it will include at least integer and floating-point literals,
with optional enclosing parentheses. It is possible that such constants will
have singleton types; see issue
[#508](https://github.com/carbon-language/carbon-lang/issues/508).
### Equivalent types
The following conversion is available:
- `T` -> `U` if `T` is equivalent to `U`
Two types are equivalent if they can be used interchangeably, implicitly: they
have the same set of values with the same meaning and the same representation,
with the same set of capabilities and constraints, where the only difference is
how the type interprets operations on values of that type.
`T` is equivalent to `U` if:
- `T` is the same type as `U`, or
- `T` is the facet type `U as SomeInterface`, or
- `U` is the facet type `T as SomeInterface`, or
- `T` is `A*`, `U` is `B*`, and `A` is equivalent to `B`, or
- for some type `V`, `T` is equivalent to `V` and `V` is equivalent to `U`.
**Note:** More type equivalence rules are expected to be added over time.
A prerequisite for types being equivalent is that they are
[compatible](../generics/terminology.md#compatible-types), and in particular
that they have the same set of values and the same representation for those
values. However, types being compatible does not imply that an implicit
conversion, or even an explicit cast, between those types is necessarily valid.
This is because the type of a value models not only the representation of the
value but also the capabilities that a user of the value has to interact with
the value. Two compatible types may expose different capabilities, such as the
capability to mutate the object or to access its implementation details, and
conversions between such types may require an explicit cast if the conversion is
possible at all.
### Pointer conversions
The following pointer conversion is available:
- `T*` -> `U*` if `T` is a subtype of `U`.
`T` is a subtype of `U` if:
- `T` is equivalent to `U`, as described above, or
- `T` is equivalent to a class derived from a class equivalent to `U`.
**Note:** More type subtyping rules are expected to be added over time.
`T*` is not necessarily a subtype of `U*` even if `T` is a subtype of `U`. For
example, we can convert `Derived*` to `Base*`, but cannot convert `Derived**` to
`Base**` because that would allow storing a `Derived2*` into a `Derived*`:
```
abstract class Base {}
class Derived extends Base {}
class Derived2 extends Base {}
var d2: Derived2 = {};
var p: Derived*;
var q: Derived2* = &d2;
var r: Base** = &p;
// Bad: would store q to p.
*r = q;
```
**Note:** If we add `const` qualification, we could treat `const T*` as a
subtype of `const U*` if `T` is a subtype of `U`, and could treat `T` as a
subtype of `const T`.
#### Pointer conversion examples
With these classes:
```
base class C;
let F: auto = C as Hashable;
class D extends C;
```
These implicit pointer conversions are permitted:
- `D*` -> `C*`: `D` is a subtype of `C`
- `F*` -> `C*`: `F` is equivalent to `C`, so `F` is a subtype of `C`
- `C*` -> `F*`: `C` is equivalent to `F`, so `C` is a subtype of `F`
- `F**` -> `C**`: `F` is equivalent to `C`, so `F*` is equivalent to `C*`, so
`F*` is a subtype of `C*`
- `D*` -> `F*`: `D` is derived from `C` and `C` is equivalent to `D`, so `D`
is a subtype of `F`
These implicit pointer conversions are disallowed:
- `C*` -> `D*`: `C` is not a subtype of `D`
- `D**` -> `C**`: `D*` is not a subtype of `C*`
Note that "equivalent to" means we can freely convert back and forwards; the
difference in the types is just changing which operations are surfaced, not
changing anything about the interpretation or switching between different
abstractions. In contrast, "subtype of" permits conversion from a more specific
type to a more general type, so the reverse conversion is not necessarily valid.
### Type-of-types
A type `T` with [type-of-type](../generics/terminology.md#type-of-type) `TT1`
can be implicitly converted to the type-of-type `TT2` if `T`
[satisfies the requirements](../generics/details.md#subtyping-between-type-of-types)
of `TT2`.
## Semantics
An implicit conversion of an expression `E` of type `T` to type `U`, when
permitted, always has the same meaning as the explicit cast expression `E as U`.
Moreover, such an implicit conversion is expected to exactly preserve the value.
For example, `(E as U) as T`, if valid, should be expected to result in the same
value as produced by `E`.
**Note:** The explicit cast expression syntax has not yet been decided. The use
of `E as T` in this document is provisional.
## Extensibility
Implicit conversions can be defined for user-defined types such as
[classes](../classes.md) by implementing the `ImplicitAs` interface:
```
interface As(Dest:! Type) {
fn Convert[me: Self]() -> Dest;
}
interface ImplicitAs(Dest:! Type) extends As(Dest) {}
```
When attempting to implicitly convert an expression `x` to type `U`, the
expression is rewritten to `x.(ImplicitAs(U).Convert)()`.
**Note:** The `As` interface is intended to be used as the implementation
vehicle for explicit casts: `x as U` would be rewritten as
`x.(As(U).Convert)()`. However, the explicit cast expression syntax has not yet
been decided, so this rewrite is provisional.
Note that implicit conversions are not transitive. Even if an
`impl A as ImplicitAs(B)` and an `impl B as ImplicitAs(C)` are both provided, an
expression of type `A` cannot be implicitly converted to type `C`. Allowing
transitivity would introduce the risk of ambiguity issues as code evolves and
would in general require a search of a potentially unbounded set of intermediate
types.
## Alternatives considered
- [Provide lossy and non-semantics-preserving implicit conversions from C++](/docs/proposals/p0820.md#c-conversions)
- [Provide no implicit conversions](/docs/proposals/p0820.md#no-conversions)
- [Provide no extensibility](/docs/proposals/p0820.md#no-extensibility)
- [Apply implicit conversions transitively](/docs/proposals/p0820.md#transitivity)
## References
- [Implicit conversions in C++](https://en.cppreference.com/w/cpp/language/implicit_conversion)
- Proposal
[#820: implicit conversions](https://github.com/carbon-language/carbon-lang/pull/820).
+33 -32
View File
@@ -144,7 +144,7 @@ properties:
just implementations of the names and signatures defined in the
`ConvertibleToString` interface, like `ToString`, and not the functions
defined on `Song` values.
- Carbon will implicitly cast values from type `Song` to type
- Carbon will implicitly convert values from type `Song` to type
`Song as ConvertibleToString` when calling a function that can only accept
types of type `ConvertibleToString`.
- In the normal case where the implementation of `ConvertibleToString` for
@@ -172,7 +172,7 @@ guaranteed to see when needed. For more on this, see
If `Song` doesn't implement an interface or we would like to use a different
implementation of that interface, we can define another type that also has the
same data representation as `Song` that has whatever different interface
implementations we want. However, Carbon won't implicitly cast to that other
implementations we want. However, Carbon won't implicitly convert to that other
type, the user will have to explicitly cast to that type in order to select
those alternate implementations. For more on this, see
[the adapting type section](#adapting-types) below.
@@ -259,8 +259,8 @@ The `impl` definition defines a [facet type](terminology.md#facet-type):
along with the `Add` and `Scale` methods, the API of `Point as Vector` _only_
has the `Add` and `Scale` methods of the `Vector` interface. The facet type
`Point as Vector` is [compatible](terminology.md#compatible-types) with `Point`,
meaning their data representations are the same, so we allow you to cast between
the two freely:
meaning their data representations are the same, so we allow you to convert
between the two freely:
```
var a: Point = (.x = 1.0, .y = 2.0);
@@ -272,7 +272,7 @@ var b: Point as Vector = a;
// `b` has `Add` and `Scale` methods:
b.Add(b.Scale(2.0));
// Will also implicitly cast when calling functions:
// Will also implicitly convert when calling functions:
fn F(c: Point as Vector, d: Point) {
d.Add(c.Scale(2.0));
}
@@ -284,7 +284,7 @@ z.Add(b);
var w: Point = z as Point;
```
These [casts](terminology.md#subtyping-and-casting) change which names are
These [conversions](terminology.md#subtyping-and-casting) change which names are
exposed in the type's API, but as much as possible we don't want the meaning of
any given name to change. Instead we want these casts to simply change the
subset of names that are visible.
@@ -300,8 +300,8 @@ of selecting an implementation of an interface for a type unambiguous throughout
the whole program, so for example `Point as Vector` is well defined.
We don't expect users to ordinarily name facet types explicitly in source code.
Instead, values are implicitly cast to a facet type as part of calling a generic
function, as described in the [Generics](#generics) section.
Instead, values are implicitly converted to a facet type as part of calling a
generic function, as described in the [Generics](#generics) section.
### Implementing multiple interfaces
@@ -405,7 +405,7 @@ can find all the names of direct (unqualified) members of a type in the
definition of that type. The only thing that may be in another library is an
`impl` of an interface.
On the other hand, if we cast to the facet type, those methods do become
On the other hand, if we convert to the facet type, those methods do become
visible:
```
@@ -413,7 +413,7 @@ var a: Point2 = (.x = 1.0, .y = 2.0);
// `a` does *not* have `Add` and `Scale` methods:
// ❌ Error: a.Add(a.Scale(2.0));
// Cast from Point2 implicitly
// Convert from Point2 implicitly
var b: Point2 as Vector = a;
// `b` does have `Add` and `Scale` methods:
b.Add(b.Scale(2.0));
@@ -552,8 +552,8 @@ fn AddAndScaleForPointAsVector(
-> Point as Vector {
return a.Add(b).Scale(s);
}
// May still be called with Point arguments, due to implicit casts.
// Similarly the return value can be implicitly cast to a Point.
// May still be called with Point arguments, due to implicit conversions.
// Similarly the return value can be implicitly converted to a Point.
var v2: Point = AddAndScaleForPointAsVector(a, w, 2.5);
```
@@ -697,10 +697,11 @@ An interface is one particularly simple example of a type-of-type. For example,
interface `Vector`. Its set of names consists of `Add` and `Scale` which are
aliases for the corresponding qualified names inside `Vector` as a namespace.
The requirements determine which types may be cast to a given type-of-type. The
result of casting a type `T` to a type-of-type `I` (written `T as I`) is called
a facet type, you might say a facet type `F` is the `I` facet of `T` if `F` is
`T as I`. The API of `F` is determined by the set of names in the type-of-type.
The requirements determine which types may be converted to a given type-of-type.
The result of converting a type `T` to a type-of-type `I` (written `T as I`) is
called a facet type, you might say a facet type `F` is the `I` facet of `T` if
`F` is `T as I`. The API of `F` is determined by the set of names in the
type-of-type.
This general structure of type-of-types holds not just for interfaces, but
others described in the rest of this document.
@@ -820,10 +821,10 @@ one generic function from another as long as you are calling a function with a
subset of your requirements.
Given a generic type `T` with type-of-type `I1`, it may be
[implicitly cast](terminology.md#subtyping-and-casting) to a type-of-type `I2`,
resulting in `T as I2`, as long as the requirements of `I1` are a superset of
the requirements of `I2`. Further, given a value `x` of type `T`, it can be
implicitly cast to `T as I2`. For example:
[implicitly converted](../expressions/implicit_conversions.md) to a type-of-type
`I2`, resulting in `T as I2`, as long as the requirements of `I1` are a superset
of the requirements of `I2`. Further, given a value `x` of type `T`, it can be
implicitly converted to `T as I2`. For example:
```
interface Printable { fn Print[me: Self](); }
@@ -1384,10 +1385,10 @@ requiring an implementation of interface `B` means `A` is more specific than
## Type compatibility
None of the casts between facet types change the implementation of any
interfaces for a type. So the result of a cast does not depend on the sequence
of casts you perform, just the original type and the final type-of-type. That
is, these types will all be equal:
None of the conversions between facet types change the implementation of any
interfaces for a type. So the result of a conversion does not depend on the
sequence of conversions you perform, just the original type and the final
type-of-type. That is, these types will all be equal:
- `T as I`
- `(T as A) as I`
@@ -1475,8 +1476,8 @@ This allows us to provide implementations of new interfaces (as in
is an equivalence class, so all of `Song`, `SongByTitle`, `FormattedSong`,
and `FormattedSongByTitle` end up compatible with each other.
- Since adapted types are compatible with the original type, you may
explicitly cast between them, but there is no implicit casting between these
types (unlike between a type and one of its facet types / impls).
explicitly cast between them, but there is no implicit conversion between
these types (unlike between a type and one of its facet types / impls).
- For the purposes of generics, we only need to support adding interface
implementations. But this `adapter` feature could be used more generally,
such as to add methods.
@@ -1545,7 +1546,7 @@ compiler provides it as
### Adapter compatibility
The framework from the [type compatibility section](#type-compatibility) allows
us to evaluate when we can cast between two different arguments to a
us to evaluate when we can convert between two different arguments to a
parameterized type. Consider three compatible types, all of which implement
`Hashable`:
@@ -1569,10 +1570,10 @@ Observe that `Song as Hashable` is different from
`Song as Hashable` and `PlayableSong as Hashable` are almost the same. In
addition to using the same data representation, they both implement one
interface, `Hashable`, and use the same implementation for that interface. The
one difference between them is that `Song as Hashable` may be implicitly cast to
`Song`, which implements interface `Printable`, and `PlayableSong as Hashable`
may be implicilty cast to `PlayableSong`, which implements interface `Media`.
This means that it is safe to cast between
one difference between them is that `Song as Hashable` may be implicitly
converted to `Song`, which implements interface `Printable`, and
`PlayableSong as Hashable` may be implicilty converted to `PlayableSong`, which
implements interface `Media`. This means that it is safe to convert between
`HashMap(Song, Int) == HashMap(Song as Hashable, Int)` and
`HashMap(PlayableSong, Int) == HashMap(PlayableSong as Hashable, Int)` (though
maybe only with an explicit cast) but
@@ -1645,7 +1646,7 @@ adapter Song extends SongLib.Song { }
external impl Song as CompareLib.Comparable { ... }
```
The caller can either cast `SongLib.Song` values to `Song` when calling
The caller can either convert `SongLib.Song` values to `Song` when calling
`CompareLib.Sort` or just start with `Song` values in the first place.
```
+8 -3
View File
@@ -371,11 +371,16 @@ its type as reflected in the API available to manipulate the value.
Casting is indicated explicitly by way of some syntax in the source code. You
might use a cast to switch between [type adaptations](#adapting-a-type), or to
be explicit where an implicit cast would otherwise occur. For now, we are saying
"`x as y`" is the provisional syntax in Carbon for casting the value `x` to the
type `y`. Note that outside of generics, the term "casting" includes any
be explicit where an implicit conversion would otherwise occur. For now, we are
saying "`x as y`" is the provisional syntax in Carbon for casting the value `x`
to the type `y`. Note that outside of generics, the term "casting" includes any
explicit type change, including those that change the data representation.
In contexts where an expression of one type is provided and a different type is
required, an [implicit conversion](../expressions/implicit_conversions.md) is
performed if it is considered safe to do so. Such an implicit conversion, if
permitted, always has the same meaning as an explicit cast.
## Adapting a type
A type can be adapted by creating a new type that is
+1
View File
@@ -71,6 +71,7 @@ request:
- [0731 - Generics details 2: adapters, associated types, parameterized interfaces](p0731.md)
- [0752 - `api` file default-`public`](p0752.md)
- [0777 - Inheritance](p0777.md)
- [0820 - Implicit conversions](p0820.md)
- [0829 - One way principle](p0829.md)
<!-- endproposals -->
+291
View File
@@ -0,0 +1,291 @@
# Implicit conversions
<!--
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
Exceptions. See /LICENSE for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-->
[Pull request](https://github.com/carbon-language/carbon-lang/pull/820)
<!-- toc -->
## Table of contents
- [Problem](#problem)
- [Background](#background)
- [Proposal](#proposal)
- [Rationale based on Carbon's goals](#rationale-based-on-carbons-goals)
- [Alternatives considered](#alternatives-considered)
- [C++ conversions](#c-conversions)
- [Array-to-pointer conversions](#array-to-pointer-conversions)
- [Function-to-pointer conversions](#function-to-pointer-conversions)
- [Qualification conversions](#qualification-conversions)
- [Integral promotions](#integral-promotions)
- [Floating-point promotions](#floating-point-promotions)
- [Integral conversions](#integral-conversions)
- [Floating-point conversions](#floating-point-conversions)
- [Pointer conversions](#pointer-conversions)
- [Pointer-to-member conversions](#pointer-to-member-conversions)
- [Function pointer conversions](#function-pointer-conversions)
- [Boolean conversions](#boolean-conversions)
- [No conversions](#no-conversions)
- [No extensibility](#no-extensibility)
- [Transitivity](#transitivity)
<!-- tocstop -->
## Problem
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.
## Background
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++](https://en.cppreference.com/w/cpp/language/implicit_conversion).
## Proposal
See changes to design.
## Rationale based on Carbon's goals
- [Software and language evolution](/docs/project/goals.md#software-and-language-evolution)
- Disallowing implicit conversions that lose information reduces the risk
that existing code will be reinterpreted in a harmful way as libraries
in use evolve.
- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write)
- Permitting a limited, safe set of implicit conversions reduces the
boilerplate work necessary to write code.
- Generics rely on performing implicit conversions between different
type-of-types for deduced type parameters. Applying the same rules
consistently for all expressions makes the language simpler.
- [Interoperability with and migration from existing C++ code](/docs/project/goals.md#interoperability-with-and-migration-from-existing-c-code)
- Providing some of the same implicit conversions as C++ reduces the need
to add explicit casts when migrating. However, explicit casts will still
be required when the C++ code was performing an operation that we don't
consider safe.
- Support for implicit conversions provides a path to expose converting
constructors and conversion functions defined in C++ code to Carbon.
## Alternatives considered
### C++ conversions
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-to-pointer conversions
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-to-pointer conversions
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.
#### Qualification conversions
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`.
#### Integral promotions
Carbon disallows implicit conversion from `bool` to integral types. We could
permit such implicit conversions.
Advantages:
- Improves C++ compatibility.
- Permits constructs to count how many of a set of predicates were true:
`if (cond1 + cond2 + cond3 >= 2)`.
Disadvantages:
- Treating truth values as the integers 0 and 1 results in code that is harder
to read and understand.
- This conversion can result in unexpected overloads being called, when a
`bool` argument is passed to a parameter of some other type.
#### Floating-point promotions
This conversion is permitted.
#### Integral conversions
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:
- Improves C++ compatibility.
- Allows implicitly undoing implicit widening in constructs such as
`char n; char c = '0' + n;` where C++ promotes `'0' + n` to `int`.
- However, Carbon is unlikely to implicitly widen to `i32` here.
Disadvantages:
- Introduces the potential for implicit data loss.
#### Floating-point conversions
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:
- Improves C++ compatibility.
- Allows implicitly undoing implicit widening in constructs such as
`float a, b; float c = a + b;` where C++ promotes `a + b` to `double`.
- However, Carbon might not implicitly widen to `f64` here.
Disadvantages:
- Introduces the potential for implicit loss of precision.
- Introduces the risk that a low-precision operation might be selected when
given higher-precision operands.
#### Pointer conversions
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.
#### Pointer-to-member conversions
Carbon does not yet have pointer-to-member types. This is out of scope for now.
#### Function pointer conversions
Carbon does not yet have function pointer types. This is out of scope for now.
#### Boolean conversions
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:
- Improves C++ compatibility and familiarity to C++ programmers.
Disadvantages:
- Harms type safety by permitting an implicit lossy conversion.
- Invites bugs where the wrong overload is selected, where an argument of
arithmetic type is passed to a `bool` parameter.
- Harms the mental model of `bool` being a choice type rather than an integer
type.
- Allowing an implicit conversion would permit this kind of conversion
everywhere, whereas it is likely only desirable in a select few places, such
as where C++ performs a "contextual conversion to `bool`".
### No conversions
We could permit no implicit conversions at all, or restrict the set of
conversions from those proposed.
Advantages:
- Code might be easier to understand, because all conversions would be fully
explicit.
Disadvantages:
- Code is likely to be harder to read and harder to write due to casts being
inserted frequently.
- Creates tension for generics, where implicit conversions between
type-of-types are a central part of the model.
### No extensibility
We could provide only built-in conversions and no user-defined implicit
conversions.
Advantages:
- Ensures that programmers don't add irresponsible implicit conversions.
Disadvantages:
- Creates an artificial distinction between built-in and user-defined types.
- Creates problems for interoperation with C++ and migration from C++, because
certain forms of user-defined implicit conversion are common in C++ code.
- Disallows useful functionality without sufficient justification.
### Transitivity
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 ...; }
}
```