Pitch: Allow interchangeable use of CGFloat
and Double
types
Implementation: PR https://github.com/apple/swift/pull/34401 based on the main
branch. Toolchains are available for macOS and Linux.
Motivation
When Swift was first released, the type of CGFloat
presented a challenge. At the time, most iOS devices were still 32-bit. SDKs such as CoreGraphics provided APIs that took 32-bit floating point values on 32-bit platforms, and 64-bit values on 64-bit platforms. When these APIs were first introduced, 32-bit scalar arithmetic was faster on 32-bit platforms, but by the time of Swift’s release, this was no longer true: then, as today, 64-bit scalar arithmetic was just as fast as 32-bit even on 32-bit platforms (albeit with higher memory overhead). But the 32/64-bit split remained, mostly for source and ABI stability reasons.
In (Objective-)C, this had little developer impact due to implicit conversions, but Swift did not have implicit conversions. A number of options to resolve this were considered:
- Keep
CGFloat
as a separate type, with explicit conversion from bothFloat
andDouble
always needed on all platforms. - Do that, but introduce specific implicit conversions from
Double
toCGFloat
. - Make
CGFloat
a typealias for eitherFloat
orDouble
based on the platform. - Consolidate on
Double
for all APIs usingCGFloat
, even on 32-bit platforms, with the Swift importer converting 32-bitCGFloat
APIs to useDouble
, with the importer adding the conversion.
One important goal was avoiding the need for users to build for both 32 and 64 bit platforms in order to know their code will work on both, so option 3 was ruled out. Option 4 was not chosen mainly because of concern over handling of pointers to CGFloat
(including arrays). Option 2 was ruled out due to challenges in the type-checker. So option 1 was chosen.
With several years’ hindsight and technical improvements, we can reevaluate these choices. 64-bit devices are now the norm, and even on 32-bit Double
is now often the better choice for calculations. The concern around arrays or pointers to CGFloat
turns out to be a minor concern as there are not many APIs that take them. And in the recent top-of-tree Swift compiler, the performance of the type-checker has significantly improved.
As the world has moved on, the choice of creating a separate type has become a significant pain point for Swift users. As the language matured and new frameworks, such as SwiftUI, have been introduced, CGFloat
has resulted in a significant impedance mismatch, without bringing any real benefit. Many newly introduced APIs have standardized on using Double
in their arguments. Because of this discrepancy, it’s not always possible to choose a “correct” type while declaring variables, so projects end up with a mix-and-match of Double
and CGFloat
declarations, with constant type conversions (or refactoring of variable types) needed. This constant juggling of types is particularly frustrating given in practice the types are transparently identical to the compiler when building for 64-bit platforms. And when building for 32-bit platforms, the need to appease the type-checker with the API at hand is the overriding driver of conversions, rather than considerations of precision versus memory use that would in theory be the deciding factor.
Proposed Solution
In order to address all of the aforementioned problems, I propose to extend the language and allow Double
and CGFloat
types to be used interchangeably by means of transparently converting one type into the other as a sort of retroactive typealias
between these two types. This is option 2 in the list above.
Let’s consider an example where such a conversion might be useful in the real world:
import UIKit
struct Progress {
let startTime: Date
let duration: TimeInterval
func drawPath(in rect: CGRect) -> UIBezierPath {
let elapsedTime = Date().timeIntervalSince(startTime)
let progress = min(elapsedTime / duration, 1.0)
let path = CGMutablePath()
let center = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: center)
path.addLine(to: CGPoint(x: center.x, y: 0))
let adjustment = .pi / 2.0
path.addRelativeArc(center: center, radius: rect.width / 2.0,
startAngle: CGFloat(0.0 - adjustment),
delta: CGFloat(2.0 * .pi * progress))
path.closeSubpath()
return UIBezierPath(cgPath: path)
}
}
Here, the Progress
struct draws a progress circle given a start time and a duration. In Foundation, seconds are expressed using TimeInterval
, which is a typealias for Double
. However, the CGMutablePath
APIs for drawing shapes require CGFloat
arguments, forcing developers to explicitly convert between Double
and CGFloat
when working with these two frameworks together. Furthermore, because float literals default to Double
in Swift, developers are forced to either explicitly annotate or convert simple constants when working with graphics APIs, such as adjustment
in the above example. With an implicit conversion between Double
and CGFloat
, the call to addRelativeArc
can be simplified to:
path.addRelativeArc(center: center, radius: rect.width / 2.0,
startAngle: 0.0 - adjustment,
delta: 2.0 * .pi * progress)
Detailed Design
The type-checker will detect all of the suitable locations where Double
is converted to CGFloat
and vice versa and allow such conversion by inserting an implicit initializer call to the appropriate constructor - (_: CGFloat) -> Double
or (_: Double) -> CGFloat
depending on conversion direction.
This new conversion has the following properties:
-
Double
is always preferred overCGFloat
where possible, in order to limit possibility of ambiguities, i.e. an overload that accepts aDouble
argument would be preferred over one that accepts aCGFloat
if both require a conversion to type-check; -
Double
↔CGFloat
conversion is introduced only if it has been determined by the type-checker that it would be impossible to type-check an expression without one; - Disallowed conversions:
- Arguments of explicit calls to the
CGFloat
initializer; - Collections: arrays, sets, or dictionaries containing
CGFloat
orDouble
keys/values have to be explicitly converted by the user. Otherwise, implicit conversion could hide memory/cpu cost associated with per-element transformations; - Explicit (conditional and checked) casts (
try
,as
etc.) and runtime checks (is
); - Any position where such a conversion has already been introduced; to prevent converting back and forth between
Double
andCGFloat
by means of other types.
- Arguments of explicit calls to the
Note that with the introduction of this new conversion, homogeneous overloads can be called with heterogeneous arguments, because it is possible to form a call that accepts both Double
and CGFloat
types or a combination thereof. This is especially common with operators.
Let’s consider following example:
func sum(_: Double, _: Double) → Double { ... }
func sum(_: CGFloat, _: CGFloat) → CGFloat { ... }
var x: Double = 0
var y: CGFloat = 42
_ = sum(x, y)
Although both overloads of sum
are homogeneous, and under current rules the call is not going to be accepted, with introduction of Double
↔ CGFloat
conversion it’s possible to form two valid calls depending of what conversion direction is picked. Since it has been established that going CGFloat
→ Double
is always preferred, sum(x, y)
is going to be type-checked as (Double, Double) -> Double
(because there is no contextual type specified) and arguments are going to be x
and Double(y)
.
The contextual type affects the preferred conversion direction since type-checker would always pick a solution with the fewest conversions possible i.e.:
let _: CGFloat = sum(x, y)
sum(CGFloat, CGFloat) -> CGFloat
is preferred in this case because it requires a single conversion versus two that would be required to make call to (Double, Double) -> Double
overload well-formed.
Source compatibility
This is an additive change and does not have any material effect on source compatibility. This change has been tested on a very large body of code, and all of the expressions that previously type-checked with explicit conversions continued to do so.
Effect on ABI stability
This change introduces new conversions at compile-time only, and so would not impact ABI.
Effect on API resilience
This is not an API-level change and would not impact resilience.
Alternatives Considered
Not to make this change and leave the painful ergonomics of the CGFloat
type intact.
A more general solution to type conversions may have provided a path to resolve this problem, but only partly. One could propose a language feature that permits implicit widening conversions – for example, from Int8
to Int
, or Float
to Double
, but not vice versa. Such a general language feature could allow user-defined (in this case CoreGraphics-defined) implicit conversion from, say, Double
to CGFloat
on 64-bit platforms, but probably not two-way conversion (CGFloat
to Double
) nor would it allow a narrowing conversion from Double
to 32-bit CGFloat
. This would result in users writing code for 64-bit platforms, but then finding their code does not compile for 32-bit platforms, and avoiding this remains a core goal for CGFloat
. So instead, we propose a custom solution for CGFloat
. Custom solutions like this are undesirable in general, but in this case the benefits to users and the ecosystem in general vastly weigh in its favor. That said, this general “implicit widening” has a lot of appeal, and the work done here with CGFloat
does prove at least that these kind of implicit conversions can be handled by the Swift type-checker as it exists today.
The choice of not converting arrays was deliberate, but does not match the current behavior with subtype conversions such as [T] as [Any]
. Since graphical code involving buffers of CGFloat
may be more performance-sensitive, explicit conversion is preferred. This need for explicit conversion will likely lead the user to a more efficient solution of building an explicitly typed array instead in the first place, avoiding a linear conversion. The memory implications on 32-bit platforms are also likely to me most material when dealing with large buffers of values, again suggesting explicit conversion is preferable. There are relatively few APIs that traffic in buffers of CGFloat
so this is not expected to be a concern.