Class convenience initializers

I understand that struct initializers can call another initializer in the same struct using self.init(...).
But in a class, only convenience initializers can do this.
What is the benefit of having this restriction on classes?

Someone else will surely fill in the details of why (I think all the info is here, though), but I'm interested—what are you not able to achieve with a combination of the required and convenience keywords?

It’s that there is something I cannot do. I’m trying understand the need for the convenience keyword. Why does the Swift compiler need us to specify that in order to allow a class initializer to call another. It isn’t needed for structs. How does the compiler benefit from requiring that keyword?

convenience init is there so you can call one of the self.inits. Normal (designated) initialisers must call through super.init; if they were allowed to call though self.init (in other words if there was no keyword or other distinction between convenience and designated inits) then you'd not have a guarantee that the base class is initialised, e.g.:

class B {
	init(thisMustBeCalled: Int) {}
}

class A: B {
	init(x: Int) {
		self.init(y: x)
	}
	init(y: Int) {
		self.init(x: y)
	}
}

It's quite logical, although a bit complicated set of rules compared to, say, C++.

structs have no "super" structs so they don't need this distinction between designated and convenience initialisers.

1 Like

Why the same error for actors, then?

actor C {
  init(_: Void) { }


  init(_: Any) { // Designated initializer for 'C' cannot delegate (with 'self.init'); did you mean this to be a convenience initializer?
    self.init(())
  }
}

Otherwise you'd be able to write:

actor C {
    init(x: Int) { }
    
    // if convenience was not required here:
    init(y: Int) {
        self.init(z: Int)
    }
    init(z: Int) {
        self.init(y: Int)
    }
}

Actually it would be a good question why we don't have something similar for structs, as this is valid:

struct A {
    init(x: Int) {
        self.init(y: x)
    }
    init(y: Int) {
        self.init(x: y)
    }
}

Should we have a similar rule for structs (convenience inits can call through self.init, designated inits can't) - it would be safer.

Edit:

Unfortunately, as one convenience initialiser is allowed to call another convenience initialiser (in other words it doesn't have to call through a designated initialiser) this situation can happen (as in the example with structs above):

class C {
    init(x: Int) {
    }
    convenience init(y: Int) {
        self.init(z: y)
    }
    convenience init(z: Int) {
        self.init(y: z)
    }
}

There's a way to fix this loophole (with both structs and convenience class initialisers) but the fix is quite cumbersome.

2 Likes

It doesn't need you to specify it just "in order to allow" a class initializer to call another. (The compiler already knows that, which must be the case given that it knows to suggest to add in the convenience keyword).

These keywords exist to communicate to readers of the code, not the compiler. Designated and convenience initializers have different meaning, that the reader should know explicitly, rather than being left to figure out for themselves.

This concept predates the keywords in Swift (or Swift in general) the concepts still were communicated informally in the API docs, with no compile-time validation. E.g.

initWithFrame:

...

Discussion

Insert the view into your window’s view hieararchy before you can do anything with it. This method is the designated initializer for the NSView class.

Here are some of the things that distinguish the two:

  1. All subclasses must implement all designated internalizes (because a convenience initializer at any level could call potentially any of them)
  2. Convenience initializers are for... well, convenience. They should typically only just adapt params (e.g. NSView.init() might call NSView.init(frame: NSRect.zero)), and not do any initialization step, because other initializers exist that could skip those steps.
  3. The designated initializer is the one that's always guaranteed to be called. It's a choke point where all initialization ends up, so it's a good place to put shared initialization logic

Whenever I forget about this stuff, I always refer back to this image, it's worth a thousand words:

4 Likes

Thanks for that explanation! I guess the same answer ("exists to communicate to the reader") applies to @escaping for callback functions. It seems the compiler already knows if a callback is escaping.

I do wish Swift could have at least chosen a shorter word than "convenience". That may hold the record for the longest keyword in any programming language. ;-)

Nice overview.

I don't understand this bullet point, as I can write:

class B {
    init(x: Int) {}
}

class C: B {
    convenience init(z: Int) {
        self.init(x: z)
    }
}

Good picture. Although it doesn't tell why sub class designated initialisers can't call convenience initialisers of superclass (i.e. what bad could happen if it was allowed).

That's going to change soon once SE-327 is fully implemented. The presence of convenience will become a warning with a fix-it that simply deletes the keyword, as it's not needed for actors because of the lack of inheritance.

Since this is pretty fresh on my mind, the compiler does make a few internal distinctions between designated and convenience initializers, so it's not purely for readers. In particular, it's an ABI break to add or remove convenience for a class, and that's published in the *.swiftinterface file for a compiled module. The compiler relies on that to know how to work with the initializer.

First, in the Swift language itself, when a programmer writes a designated initializer, they're responsible for initializing all of the stored properties of self before making any other use of it. That contract is checked by the compiler in the same manner as if you had declared a local variable without an initializing expression. Convenience (or delegating) inits differ in that they must delegate to a designated initializer of the same class (as the diagram depicts) before any uses of self or returning.

In terms of implementation details, the two kinds of initializers in Swift have some differences. Designated initializers have two entry-points, whereas convenience inits only have one (which does all of the things that the designated init does in one function). Why? I believe it has to do with a combination of memory allocation responsibilities, the need for dynamic type metadata to do the allocation (in some cases), and hold-overs from how Objective-C chains initialization of a subclass to a superclass.

The first entry-point of a designated initializer performs allocation by accepting dynamic type information as a parameter (as passed-in by the caller), which can be used to describe the type's size and layout in order to allocate a correctly sized instance, etc. That first entry-point gets published in the class's vtable, so that entry-point is what you're really overriding in a class. From what I've seen in the compiler, that type parameter is definitely used by Obj-C classes, as all of the Swift classes I've tested knows its type statically and uses that, ignoring the dynamic type argument.

Now, the second entry-point of a designated initializer is where the body of the designated initializer, as it appears in the original source, is actually emitted. But, instead of accepting type metadata as a parameter, this entry-point accepts an instance of that class itself for it to initialize. So it's kind of like this:

// MyClass._allocating_init(designated:)
sil_func MyClass_allocating_init_designated(..., type: TypeMetadata) {
  let self = // ... do memory allocation, etc ...
  MyClass_init_designated(..., self)
}

// MyClass.init(designated:)
sil_func MyClass_init_designated(..., self: MyClass) {
  /// do initialization
}

// MyClass._allocating_init(convenience:)
sil_func MyClass_init_convenience(..., type: TypeMetadata) {
  /// delegate to an init
  /// do any further initialization
}

That is really the key difference between designated and convenience. The first point where you enter a class's initializer determines the type metadata for the instance to be allocated (as passed-in by the caller, to support generics, etc). After that, the initialization chaining for super-classes goes through these secondary entry-points, so that the super-classes only need to initialize the parts of the instance it knows about, knowing nothing about the underlying instance that was passed in.

To make it a bit more visual, it's sort of like this:

/// Superclass
+---------------------------------------------------+
|             Designated init                       |
|  [ allocating entry ] --> [ initializing entry ]  |
|                                        ^
+----------------------------------------|----------+
                                         |
                                         |  super.init(...)
// Subclass                              |
+--------------------------------------- | ---------+
|             Designated init            |          |
|  [ allocating entry ] --> [ initializing entry ]  |
+---------------------------------------------------+

Why not always emit two entrypoints to eliminate the distinction? I don't know. This is probably tied to some history or other design trade-offs that I'm not aware of.

Also, in theory it shouldn't need to be an ABI break to add/remove convenience for a final class, but that does appear to be the case for a reason I haven't investigated yet.

3 Likes

"Why does Swift need the `convenience` keyword?" has my [former Swift compiler developer] answers to most of the questions that have come up here. Actors are like classes because the actor proposal didn't want to rule out actor subclassing (sub-actoring?).

7 Likes

I think that's because your example falls under rule 1 for Automatic Initializer Inheritance. So that means class C gets init(x: Int) via inheritance, so it has that designated initializer, i.e. the convenience one calls that via self (and not super).

2 Likes

@AlexanderM Do you have a source (link) for this diagram? Is this from the Swift documentation? Thanks!

Official links
https://docs.swift.org/swift-book/LanguageGuide/Initialization.html

2 Likes

Thanks. I was wondering because the style of the diagram posted by @AlexanderM is totally different. Probably from an old version of the docs.

I just found It through an image search. It was hosted on some blog, but I think the original origin is one of apple’s “Programming Guides” (gosh I wish they still made those)

1 Like