The Behavior of Super

(I recently posted this thread to Apple’s objc-language mailing list. I welcome feedback there or here.)

A recent Twitter conversation spurred me to think about how we use super in Objective-C.

I have two main problems with the current functionality of super:

  1. It can only be used to dispatch to the immediate superclass.It’s sometimes useful to skip the superclass’s implementation from within an override, calling the grandparent class’s implementation directly. (Bug workarounds are the primary use case that comes to mind.) This can’t be done with the super keyword, but the underlying objc_msgSendSuper runtime call can express this without any difficulty.I propose that the super keyword be extended to take an optional class argument. For example, given the following code:
 
@implementation Foo : NSObject
- (NSString *)description {
  return @"Foo description";
}
@end

@implementation Bar : Foo
- (NSString *)description {
  return [super(NSObject) description];
}
@end
 

Bar‘s implementation of -description will call NSObject‘s instead of Foo‘s. (edit: Thanks to Tim Ekl for pointing out a typo in the above example.)

The compiler would produce an error if the provided class is not an ancestor of the class on which the calling method is defined.

It would also make sense to extend this syntax to support references to grandparent classes without actually having to know the class hierarchy at authoring time, which would be particularly useful for macros. super(super) could refer to the grandparent class, extensible to any nesting of supers. The syntax for a super receiver becomes:

SuperReceiver = "super" "(" Identifier ")" | SuperChain
SuperChain    = "super" | "super" "(" SuperChain ")"

(I can’t think of any reasonable semantics for super(super(SomeClass)), so my syntax excludes it.)

In keeping with the current semantics of super, which uses the compile-time declared superclass to construct an objc_super struct, the compiler would evaluate the super chain at compile time, based on the superclasses declared in the superclasses’ @interface blocks.

  1. -respondsToSelector: et al. don’t behave as naïve users expect.New Objective-C programmers who are unfamiliar with message-passing style often write code like [super respondsToSelector:@selector(awakeFromNib)]. Unless self overrides -respondsToSelector:, this is exactly equivalent to [self respondsToSelector:].There are a couple of ways to improve this:
  • Make -respondsToSelector:-conformsToProtocol:, and other methods behave specially when sent to super.This is certainly the more radical approach: make the behavior conform to expectation. Well, at least the expectation of the naïve user—the experienced programmer might very well be confused by this change.I’ve written a quick implementation of this approach which is partially inspired by Python’s super class. Instead of using the super keyword, you send -super to self and then message the returned proxy as you would super. The proxy takes care of forwarding invocations to objc_msgSendSuper. (My implementation only covers register-returning messages on x86_64, but it could be very easily extended.) There is also a -superOfClass: method that lets you invoke methods of higher ancestors, as would the super() syntax described above.Since it doesn’t really make sense to message super unless you intend to invoke a superclass implementation, I believe the only code whose semantics would be affected by this change is already erroneous. If the language were to adopt this feature, it wouldn’t necessarily need to use an object—super could remain a positional keyword with the enhanced semantics. In fact, it would probably best if it didn’t, so that one could not accidentally return an instance of the proxy to other code, resulting in strange and hard-to-debug misbehavior.One might argue that this change is unnecessary, as it sacrifices both source compatibility and the elegance of super‘s definition to provide another means to invoke existing behavior (for example, by using [[self superclass] instancesRespondToSelector:] instead of the erroneous [super respondsToSelector:]). But I think that it’s a bad idea for a language to permit something to obviously wrong, much less make it so attractive. It’s possible to reconcile those two opinions, however, with a different approach:
  • Emit warnings for super calls which the compiler believes are erroneous.At the site of a message to super, if the compiler does not see an override of the message being sent, it’s a pretty good chance that the programmer does not actually want the semantics they’re asking for. So the compiler could emit a warning:
@implementation MyView
- (void)awakeFromNib {
  if ([super respondsToSelector:_cmd]) { // warning: no declaration of -[MyView
                                         // respondsToSelector:] found. This message
                                         // is likely equivalent to `[self
                                         // respondsToSelector:]`.
    ...
  }
}
@end

The warning could even be smart for particular variants of this bug, suggesting use of [self superclass] for common messages like -respondsToSelector:.

This option is much less invasive, but has some downsides. It will issue false warnings on code that attempts invoke the superclass implementation of a method whose declaration is invisible from the call site. I think this is probably a rare scenario, and is otherwise fixable by importing the right header or adding a category declaration.

I’m not sure what the line is between compiler warnings and analyzer warnings, but this seems like a fairly low-cost warning that would catch real bugs.

Honestly, while I’m proud of the super-as-class implementation, I’m not that attached to it. The warning seems like a far more achievable goal. But I’m certainly open to being convinced otherwise.