A reasonable question, if one more on the implementation side than the first! Internally Swift has the notion of “allocating initializers”, which allocate memory, and “non-allocating initializers”, which just initialize it.* An allocating initializer is essentially [[ThisClass alloc] init…]
, packed up in a method for a handful of reasons but I think mainly to save code size at the call site. The base rules for this:
- If you instantiate a class statically, you’re calling an allocating initializer.
- If you call
super.init
, you don’t want a new allocation, so you’re calling a non-allocating initializer.
- A
required
initializer can be called on an unknown subclass, so the dynamic entry point for this is an allocating initializer. (Otherwise you’d have to look up the class’s layout separately, which doesn’t buy us much.)
Looking at this, you can see that the only time you need non-allocating entry points is for subclasses to call with super
. So pure-Swift convenience initializers are also allocating, since they can delegate to an allocating entry point. Now call sites never have to do allocation themselves, and if a non-allocating initializer isn’t part of an open
class, it doesn’t need to be exposed outside the library at all, and may be inlined away. (We’ll come back to this later…)
Objective-C makes this a bit more complicated:
-
Objective-C doesn’t provide allocating initializers, so Swift synthesizes them when you use them to instantiate an ObjC class. These still call +alloc
; you can think of it as factoring out common code. (And I think they can be inlined back in.)
-
Objective-C designated init methods behave like non-allocating initializers…most of the time. But sometimes they replace self
instead. So Swift subclasses of ObjC subclasses have to be prepared for this possibility, and carefully deinitialize the already-initialized properties in the old instance. I forget if this logic lives in the constructor or in the deinitializer, but it was a pain to work out.
-
Objective-C convenience init methods are non-allocating! Because they directly call non-allocating designated init methods! Because in Objective-C, init methods are just instance methods and the compiler doesn’t treat them specially! Or didn’t before ARC, anyway. So Swift has to patch over this too when exposing a Swift convenience init to Objective-C: in this case the initial result from +alloc
is thrown away and the Swift convenience init implementation will do its own allocation.
-
Finally, while it’s extremely rare, Objective-C classes can override +alloc
, and Swift subclasses of ObjC classes will respect that instead of using the default Swift allocator.
But there’s yet another twist thrown in here: stack promotion. If Swift statically knows the layout of a class, and can prove that it doesn’t escape, it can allocate the class on the stack instead of the heap. Oops, all your allocating entry points are useless; you need the non-allocating ones again! Or do you? Because initializers can save a reference to self
off somewhere, they already have to be inlinable to check that the object doesn’t escape, which means the compiler can, say, copy everything in the allocating initializer into a new function that skips the allocation. (Disclaimer: I don’t know how stack promotion is actually implemented; I didn’t work on that part of the compiler. But it does work!)
If we wanted to support stack promotion for non-inlinable initializers in other modules (say, with a new attribute @unsafePromiseThatSelfDoesNotEscape
), we’d have to revisit some of this. Same for initializing specific memory locations, instead of relying on Swift’s allocator (tricky because you have to handle deallocation safely as well). There are reasons to want to do both of those but I think they’re relatively niche.
With all this complexity, should Swift have stuck to the ObjC/C++ model instead, at least at the implementation level? I don’t know. The ObjC model is certainly more flexible, at the cost of some code size in the vastly most common case. There may be some additional optimization opportunities in the future that come from the allocation logic living in the library rather than its clients (for instance, language-level object pooling). I’m not an expert in this area of the language implementation. But hopefully I’ve explained the mechanisms as they are and how they got that way. :-)
* You’ll often see initializers referred to in the compiler internals as “constructors”, which reflects both the influence of Clang’s C++ implementation and the additional DI constraints that ObjC init methods don’t have. In very early builds of Swift pre-1.0 the keyword was actually constructor
instead of init
! I’m glad we changed that one.