objc_msgSendSuper
Precisely distinguish [self method] from [super method] at the ABI level: both send a message to the exact same receiver, but only [super method] changes where method LOOKUP begins, starting it at a superclass instead of the receiver's actual class. Understand the compiler-generated objc_super struct that objc_msgSendSuper actually takes in place of a plain receiver, and the common polymorphism surprise that results from forgetting self's identity never changes.
The companion lesson objc_msgSend in Depth traced the ordinary message-dispatch path in full: a receiver and selector go in, the receiver's actual class's method cache and method list are consulted, and an IMP comes out. This lesson covers the one deliberate variation on that path Objective-C provides: [super method]. It builds directly on that companion lesson and on the earlier Inheritance and Objective-C Runtime lessons, and it exists because [super method] is, by a wide margin, the most commonly misunderstood piece of syntax involving inheritance in the entire language -- most learners' first guess about what it does is subtly, importantly wrong.
The Most Commonly Misunderstood Fact About super
Here is the fact this entire lesson is built around, stated plainly before anything else: [super method] does NOT change who receives the message. The receiver is exactly the same object it would have been for an ordinary [self method] send -- self inside whatever implementation eventually runs is still that same original object, with the same actual runtime class it always had.
What [super method] actually changes is where method LOOKUP begins searching for an implementation to run. An ordinary [self method] send starts looking for method at the receiver's own actual (dynamic) class and walks upward from there only if needed. A [super method] send skips that first step entirely and starts looking one level higher up, at the superclass of the class whose code contains the [super ...] call -- but it is still looking for an implementation to run against that same, unchanged receiver.
Many learners' intuition is that super somehow 'becomes' the object, or that code reached via super is somehow running 'as if it were' an instance of the superclass. It is not. The object never changes. Only the starting point of the search for which code to run changes. Keeping this distinction sharp is what the rest of this lesson, and its diagram, are built to reinforce.
• [super method] sends to the exact same receiver as [self method] would -- the object itself never changes. • What changes is where method LOOKUP starts: at the superclass, rather than at the receiver's own actual class. • The common misconception is that super changes what object receives the message -- it does not; it only changes where the search for an implementation begins.
[self method] vs. [super method]: The Diagram
The diagram accompanying this lesson lays the two side by side in the simplest possible form. [self method] sends a message to the current receiver -- lookup, as the companion objc_msgSend in Depth lesson covers in full, begins at that receiver's own actual class and walks upward only on a cache miss or method-list miss.
[super method] also sends a message -- to that exact same current receiver -- but lookup begins one level higher up: at the superclass, skipping the receiver's own class's method list entirely for this one particular call. If the superclass doesn't implement the method either, lookup continues walking upward from there exactly the way an ordinary send would, just starting one rung higher on the ladder.
• [self method]: message to the current receiver, lookup starts at the receiver's own actual class. • [super method]: message to that SAME current receiver, lookup starts at the superclass instead. • From the superclass onward, if lookup doesn't find a match there either, it continues walking upward exactly as an ordinary send would.
A Concrete Example: Overriding and Calling Super
Consider an Animal class with a describe method, and a Dog subclass that overrides describe and calls [super describe] from inside its own override. When code calls [rex describe] on a Dog instance named rex, Dog's override runs first (because rex's actual class is Dog, and ordinary dispatch finds Dog's own override immediately). Inside that override, [super describe] sends describe to rex again -- the identical object -- but with lookup starting at Animal instead of Dog, which is exactly where Animal's own implementation of describe is found and runs.
The detail worth tracing carefully is what self is while Animal's implementation of describe is executing. It is not some freshly-constructed Animal instance, and it is not 'rex, temporarily treated as an Animal.' It is rex itself, completely unchanged -- the very same Dog object, with the very same actual runtime class Dog it always had. Sending [self class] from inside Animal's own describe implementation, reached this way, reports Dog, not Animal, because self's actual identity and actual class never moved.
• Dog's override of describe runs first for [rex describe], because ordinary dispatch finds it starting at rex's own actual class, Dog. • [super describe] inside that override sends describe to rex again (the same object), with lookup starting at Animal. • Inside Animal's implementation, self is still rex -- an actual Dog instance -- not some separate Animal-typed object.
The objc_super Struct: What objc_msgSendSuper Actually Takes
The companion objc_msgSend in Depth lesson showed that an ordinary [self method] call compiles down to objc_msgSend(receiver, selector, ...) -- the receiver passed directly as the first argument. A [super method] call compiles down to a different function entirely, objc_msgSendSuper, and it does not take a plain receiver as its first argument at all.
Instead, the compiler builds a small struct, objc_super, containing exactly two fields: receiver, set to the unchanged, actual object the message is being sent to (rex, in the earlier example), and super_class (or class, depending on runtime version), set to the class where lookup should begin. Critically, that starting class is determined lexically, at compile time, by which class's source code contains the [super ...] call -- it is the superclass of Dog, because the call appears inside Dog's own implementation of describe -- not by anything about the receiver's actual dynamic class, which merely happens to also be Dog in this particular example but need not be in a deeper hierarchy.
objc_msgSendSuper is then called with a pointer to this objc_super struct, plus the same selector and any further arguments an ordinary objc_msgSend call would take. The runtime uses the struct's receiver field as the actual object the found implementation ultimately runs against, and uses its super_class field purely to decide where lookup starts searching -- exactly matching the diagram's two-part description of what a super send does.
• objc_msgSendSuper takes a pointer to an objc_super struct instead of a plain receiver as its first argument. • objc_super has two fields: receiver (the unchanged, actual object) and super_class (the class where lookup should begin). • super_class is determined lexically -- by which class's source code contains the [super ...] call -- not by the receiver's actual dynamic class. • The struct's receiver field is what the found implementation ultimately runs against; its super_class field only affects where the search begins.
Comparing objc_msgSend and objc_msgSendSuper Side by Side
Placed next to each other, the difference between the two functions is precise and narrow. objc_msgSend(receiver, selector, ...) starts method lookup at receiver's own actual, dynamic class -- whatever class the object genuinely is at runtime. objc_msgSendSuper(&superStruct, selector, ...), where superStruct is { receiver, someClass }, starts lookup at someClass instead -- a class chosen at compile time by where the calling code lives -- while still ultimately invoking the found implementation against that same receiver.
Put another way: objc_msgSend answers 'find whatever this object's own class (or something above it) actually implements.' objc_msgSendSuper answers 'find whatever a SPECIFIC class (or something above it) implements, but still run it against this particular object.' The receiver argument is identical in both; only the starting point for the search differs, and only objc_msgSendSuper lets that starting point be something other than the receiver's own actual class.
• objc_msgSend: lookup starts at the receiver's own actual (dynamic) class. • objc_msgSendSuper: lookup starts at a specific class named in the objc_super struct, chosen lexically at compile time -- not the receiver's own dynamic class. • Both ultimately invoke the found implementation against the exact same receiver -- the only difference is where the search for that implementation begins.
The Polymorphism Surprise: When a Superclass Implementation Calls Back Into self
Here is the subtle bug this fact makes possible. Suppose Base declares two methods, run and step, and Base's own implementation of run calls [self step] internally -- an entirely ordinary message send, not a super send. Suppose Derived overrides both run (calling [super run] to reach Base's implementation) and step.
When [d run] is called on a Derived instance d, Derived's override of run runs first, and it calls [super run], which starts lookup at Base and finds Base's own run. So far, this matches everything covered above: self inside Base's run is still d, an actual Derived instance. But Base's run then calls [self step] -- and that inner call is an ORDINARY message send, not a super send, so its lookup starts fresh at self's actual dynamic class, which is Derived. The result is that Derived's override of step runs, not Base's own step -- even though execution is, at that moment, 'inside' code that was reached via [super run].
This surprises people specifically because being 'inside the superclass's code' feels like it should mean the superclass's own methods are used consistently throughout that code -- but that is exactly the misconception this lesson opened by rejecting. Only the ONE super send itself changes where lookup begins; everything that call goes on to do internally, including any further messages it sends to self, resumes completely normal, fully polymorphic dispatch based on self's real, unchanged dynamic class. A superclass implementation that calls back into its own overridable methods on self is deliberately inviting subclasses to influence its behavior this way -- which is sometimes exactly the intended design (it's the basis of the template method pattern) and sometimes a genuine, hard-to-spot bug when it isn't.
• A superclass method reached via [super ...] can still call other methods on self using ordinary sends -- and those sends dispatch normally, based on self's real dynamic class. • This means a subclass's override of an inner method CAN run even while execution is 'inside' code reached through a super call to a different method. • This is sometimes intentional design (the template method pattern) and sometimes a subtle, surprising bug -- the fix is recognizing that self's polymorphism never pauses, even inside superclass code.
Code Examples
[super describe] inside Dog's implementation runs Animal's own code, but the receiver is still rex -- so [self class] evaluated inside Animal's describe reports Dog, not Animal. The object never changed; only the starting point for finding an implementation of describe moved up to Animal for this one call.
NSString *base = [super describe];
This is the single most important observation in the whole lesson made concrete: even standing inside Animal's own source code, self still reports its true, unchanged runtime class, Dog. Nothing about calling code via super converts the receiver into an instance of the superclass.
NSLog(@"[self class] inside Animal's describe = %@", NSStringFromClass([self class]));
super_class is set to Animal because [super describe] appears lexically inside Dog's own implementation -- it is determined by which class's source code contains the call, not by the receiver's actual dynamic class (which, in this particular example, also happens to be Dog, but that's incidental). receiver is passed through completely unchanged, exactly matching the diagram: same receiver, different lookup start.
struct objc_super superInfo = { self, class_getSuperclass([Dog class]) };
Both calls act on the identical receiver -- self, unchanged in either case. The only structural difference is that objc_msgSend takes the receiver directly as its first argument (so lookup starts at whatever class that receiver actually is), while objc_msgSendSuper takes a struct bundling the receiver together with an explicit starting class for lookup, chosen by the compiler based on where the [super ...] call appears in source.
objc_msgSendSuper(&superInfo, @selector(describe));
[super run] correctly reaches Base's implementation of run -- but the [self step] call inside Base's own run is an ordinary send, not a super send, so it dispatches based on self's real dynamic class, Derived, running Derived's override of step instead of Base's own. Being 'inside' code reached through super does not pause self's normal polymorphism for anything that code goes on to do afterward.
[self step]; // ordinary send inside Base's own run -- still dispatches on self's REAL class, Derived
Common mistake: Assuming that code reached via [super run] will itself only ever call the superclass's own versions of other methods -- any further [self someMethod] sends inside it dispatch completely normally, based on the receiver's real dynamic class, and can therefore run a subclass's override.
@interface Animal : NSObject
- (NSString *)describe;
@end
@implementation Animal
- (NSString *)describe {
return [NSString stringWithFormat:@"An Animal, class = %@",
NSStringFromClass([self class])];
}
@end
@interface Dog : Animal
- (NSString *)describe;
@end
@implementation Dog
- (NSString *)describe {
NSString *base = [super describe];
return [NSString stringWithFormat:@"%@ (specifically a Dog)", base];
}
@end
Dog *rex = [[Dog alloc] init];
NSLog(@"%@", [rex describe]);
// "An Animal, class = Dog (specifically a Dog)"@implementation Animal
- (NSString *)describe {
NSLog(@"[self class] inside Animal's describe = %@",
NSStringFromClass([self class]));
return @"Animal";
}
@end
// Calling [rex describe] on a Dog instance (via Dog's override calling
// [super describe]) prints:
// "[self class] inside Animal's describe = Dog"
//
// self is unchanged -- it's still the very same Dog object -- only
// lookup for THIS particular call started at Animal instead of Dog.#import <objc/message.h>
#import <objc/runtime.h>
// What the compiler generates, conceptually, for [super describe] inside
// Dog's own -describe implementation:
struct objc_super superInfo = {
.receiver = self, // unchanged: still the real Dog instance
.super_class = class_getSuperclass([Dog class]) // lookup starts at Animal
};
NSString *base =
((NSString *(*)(struct objc_super *, SEL))objc_msgSendSuper)(
&superInfo, @selector(describe));// Ordinary send: lookup starts at self's ACTUAL runtime class.
objc_msgSend(self, @selector(describe));
// Super send: lookup starts at the given super_class instead, but the
// RECEIVER (the object the found implementation ultimately runs
// against, and what 'self' is set to inside that implementation) is
// exactly the same self.
struct objc_super superInfo = { self, class_getSuperclass([Dog class]) };
objc_msgSendSuper(&superInfo, @selector(describe));@interface Base : NSObject
- (void)run;
- (void)step;
@end
@implementation Base
- (void)run {
NSLog(@"Base run: about to call step");
[self step]; // an ORDINARY send -- resumes normal dynamic dispatch
// on self's real, actual class
}
- (void)step {
NSLog(@"Base step");
}
@end
@interface Derived : Base
@end
@implementation Derived
- (void)run {
NSLog(@"Derived run: calling super");
[super run]; // lookup for THIS send starts at Base
}
- (void)step {
NSLog(@"Derived step (override)");
}
@end
Derived *d = [[Derived alloc] init];
[d run];
// Output:
// "Derived run: calling super"
// "Base run: about to call step"
// "Derived step (override)" <-- NOT "Base step"!self vs. super: Same Receiver, Different Lookup Start
Both sends target the exact same receiver -- the object itself never changes. [self method] starts looking for an implementation at the receiver's own actual class; [super method] starts looking one level higher, at the superclass, while still ultimately running the found implementation against that same, unchanged receiver.
[self method]
↓
message to current receiver
[super method]
↓
message lookup begins at superclassWhere This Distinction Actually Bites
The [self step]-inside-[super run] pattern shown above is not a contrived edge case -- it is the exact mechanism behind the template method design pattern, where a base class deliberately defines a fixed overall algorithm in terms of calls to its own overridable 'hook' methods, expecting and relying on subclasses to override those hooks. Used intentionally, this is a normal, powerful pattern. Used unintentionally -- most commonly when a class is refactored to introduce an override of a method that an inherited implementation already calls internally -- it silently changes the inherited method's behavior in a way that is easy to miss during code review, because nothing about the base class's own source code changed at all. Anywhere a codebase relies on calling [super someMethod] and that superclass implementation goes on to send further messages to self, it's worth explicitly checking whether every one of those inner methods is meant to be overridable by the calling subclass -- if not, the base class needs a design that doesn't route through self at all for that particular call.
@implementation Base - (void)run { NSLog(@"Base run: about to call step"); [self step]; // looks like it should call Base's OWN -step, // since we're 'inside' Base's code... } - (void)step { NSLog(@"Base step"); } @end // Derived overrides -step, then calls [super run] from its own -run. // A developer debugging this expects to see "Base step" printed, and // is confused when "Derived step (override)" appears instead.
@implementation Base - (void)run { NSLog(@"Base run: about to call step"); // [self step] is genuinely, correctly polymorphic here -- if Base's // OWN -step is truly required regardless of what any subclass // overrides, that intention needs to be made explicit, for example // by giving Base's own version a distinct, non-overridable name // that -step then calls, rather than relying on [self step] to stay // 'pinned' to Base just because the surrounding code lives in Base: [self step]; } - (void)step { NSLog(@"Base step"); } @end self inside any method -- including one reached via [super...] from a subclass -- is still the receiver's real, unchanged dynamic type. An ordinary [self step] call there is fully polymorphic and will run a subclass's override if one exists; only an explicit [super step] call, written inside Base's own code, would force lookup to start at Base itself for that inner call too.
What is the single most important fact to understand about [super method]? A. It sends the message to a newly created instance of the superclass instead of the original receiver B. It sends the message to the exact same receiver as an ordinary send would -- only WHERE method lookup begins changes, not WHO receives the message C. It is purely a compile-time-only construct with no effect on which code actually runs D. It always calls every superclass's implementation of the method, all the way up to the root class, in sequence
B. It sends the message to the exact same receiver as an ordinary send would -- only WHERE method lookup begins changes, not WHO receives the message — The receiver is identical in both [self method] and [super method] -- self's actual runtime class never changes. super only changes the starting point the runtime uses when searching for an implementation to run against that same, unchanged receiver.
Inside a superclass's implementation reached via [super someMethod], what does [self class] report? A. The superclass, since execution is 'inside' the superclass's own code B. The receiver's actual, real runtime class -- unchanged from what it was before the super call, even if that's a further subclass C. id, since the type becomes generic once inside superclass code D. It throws a runtime exception, since self's class is ambiguous inside a super call
B. The receiver's actual, real runtime class -- unchanged from what it was before the super call, even if that's a further subclass — self's identity and actual class never change as a result of a super call -- only where lookup started for that one particular message differs. [self class] always reports the receiver's true, unchanged dynamic class.
What two fields does the compiler-generated objc_super struct contain, and what does each control? A. A class name string and a method name string, used to print debug logs B. receiver (the unchanged, actual object the message is sent to) and super_class (the class where method lookup should begin) C. A retain count and a weak-reference flag D. The selector being sent and the number of arguments it takes
B. receiver (the unchanged, actual object the message is sent to) and super_class (the class where method lookup should begin) — objc_super bundles exactly the two pieces of information a super send needs: the unchanged receiver (so the found implementation still runs against the right object) and the class where lookup should start searching (determined lexically, by where the [super ...] call appears in source).
How is the super_class field of an objc_super struct determined? A. By the receiver's actual dynamic class at the moment the message is sent B. By which class's source code lexically contains the [super ...] call -- it is that class's superclass, chosen at compile time C. By asking the user to specify it explicitly as an extra argument every time D. It is always NSObject, regardless of where the call appears
B. By which class's source code lexically contains the [super ...] call -- it is that class's superclass, chosen at compile time — super_class is fixed at compile time based on where the [super ...] call is written in source -- the superclass of the class whose implementation contains that call -- not by inspecting the receiver's actual dynamic class at runtime.
A Base class's own -run implementation calls [self step] (an ordinary send). Derived overrides both -run (calling [super run]) and -step. When [derivedInstance run] executes, which -step implementation actually runs, and why? A. Base's -step, because execution is 'inside' code reached via [super run] B. Derived's -step override, because [self step] is an ordinary send that dispatches based on self's real, unchanged dynamic class (Derived), regardless of which class's source code the call happens to be written in C. Neither -- calling [self step] from inside code reached via super is a compile error D. It alternates between the two on successive calls
B. Derived's -step override, because [self step] is an ordinary send that dispatches based on self's real, unchanged dynamic class (Derived), regardless of which class's source code the call happens to be written in — Only the one super call itself changes where lookup starts. The [self step] call inside Base's -run is a completely ordinary send, so it resumes normal, fully polymorphic dispatch on self's actual class -- Derived -- running Derived's override, not Base's own -step.
Write three classes: Shape (declaring and implementing -area returning 0 and -describe, where -describe calls [self area] and formats it into a string), Circle (a subclass of Shape overriding only -area), and a short piece of calling code that creates a Circle and calls -describe on it. Trace, in a comment, exactly which -area implementation runs when -describe executes, and explain why, referencing self's identity.
@implementation Shape\n- (double)area { return 0; }\n- (NSString *)describe {\n return [NSString stringWithFormat:@"area = %f", [self area]];\n}\n@end\n\n@implementation Circle\n- (double)area { return 3.14159 * self.radius * self.radius; }\n@end\n\nCircle *c = [[Circle alloc] init];\nc.radius = 2;\nNSLog(@"%@", [c describe]);\n// Circle doesn't override -describe, so [c describe] runs Shape's own\n// -describe via ordinary inherited dispatch. Inside it, [self area] is\n// an ordinary send evaluated against self's real class -- Circle -- so\n// Circle's -area runs, not Shape's, printing a nonzero area.
describe is inherited by Circle unchanged (Circle doesn't override it), so calling [circle describe] finds Shape's own -describe through ordinary dispatch on Circle's actual class -- but inside that inherited -describe, [self area] is still an ordinary send evaluated against self's real class, which is Circle.