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.