Advanced programming tips, tricks and hacks for Mac development in C/Objective-C and Cocoa.

Variable argument lists in Cocoa

This week I'll talk about methods that take variable numbers of arguments, also known as variadic methods. I'll show you the Objective-C syntax and implementation, give a quick rundown of the ways that Cocoa classes provide variable argument support and I'll also show you a way to fake va_list parameters to handle Cocoa's variadic method equivalents at runtime.

Variable arguments basics

Passing a variable number of arguments to a method is a convenient way to handle a list of variables that are in scope at compile time.

The Objective-C language handles variable arguments in the same way that Standard C does. Normally, you will encounter variable argument lists in one of two forms: "Format strings" or "Nil terminated lists".

Format strings

The traditional example for variable arguments are format strings. Format strings contain a number of "placeholders" (escape sequences starting with a "%" sign) that are replaced with data from variables when the format string and the variable arguments are passed to the method.

NSString *myString = [NSString stringWithFormat:
    @"Number %d, String: %@, Float: %g", 123, @"SomeString", 34.5];

This method is declared as follows:

+ (id)stringWithFormat:(NSString *)format, ...;

The "..." is the variable argument.

This method declares a first parameter but everything else is "variable". This does not mean optional. Instead, it means that a number of extra parameters will be required. Exactly what number and their types depends on how the method works.

The documented behavior for stringWithFormat:, is that it scans the format string and requires 1 variable argument for every escape sequence (in this case there are 3) and the type must match the specifier for each escape sequence (in this case, "d", "@" and "g" specify int, id and float/double).

Nil terminated lists

The other common type of variable argument list method is one that takes a list of objects terminated by nil.

In Cocoa, these are commonly used for constructing collection objects.

NSArray *myArray = [NSArray arrayWithObjects:@"One", @"Two", @"Three", nil];

The documented rule for this type of method is that the last argument must be nil.

Getting argument numbers wrong

The details of "Format strings" and "Nil terminated lists" reveal the difficulty with variable argument lists in Objective-C: knowing how many arguments exist — i.e. when to stop reading arguments.

Objective-C, like C, does not have a runtime argument count, nor does it pass runtime argument types. So the number and types of arguments must be determined by some other policy. In the case of these two examples, the argument count is, respectively: the number of escape sequences in the "Format string" or the number of arguments before the first nil. For "format strings" the types are determined by the escape sequences, for "Nil terminated" lists, the arguments are always pointers.

The penalty for getting variable argument list wrong can be severe: the compiler won't typically notice a problem and your code will crash or behave very strangely at runtime. The important lesson to learn is that you should only use variable arguments in a situation where the policy for numbers and types of arguments is very clear.

You can improve the compiler verification of variable argument lists by using the -Wformat compiler flag in GCC. See below for more about how this works.

Implementing variable arguments for your own methods

Lets look at the implementation for the example class:

@interface StringContainer : NSObject
{
    NSString *contents;
}
@end

Imagine we wanted a method that would set the contents string this class contains by concatenating a variable argument list of strings together.

In this case, we will use the nil terminated approach — the list of strings to concatenate will end with a nil, so we know when to stop reading.

The declaration for the method looks like this:

- (void)setContentByAppendingStrings:(NSString *)firstString, ...
    NS_REQUIRES_NIL_TERMINATION;

The NS_REQUIRES_NIL_TERMINATION part is a macro that tells the compiler that invocations of this method must include a nil-terminated list of arguments. Failure to nil-terminate the list will result in a compiler warning if -Wformat is enabled.

Annoyingly, -Wformat is not enabled by default. Double annoyingly, it is named "Typecheck Calls to printf/scanf" in the "GCC 4.0 Warnings" section of the "Build Settings" in Xcode, even though this setting affects the NS_REQUIRES_NIL_TERMINATION macro which is used commonly throughout all of Cocoa — far beyond printf and scanf.

The implementation of this method is as follows:

- (void)setContentByAppendingStrings:(NSString *)firstArg, ...
{
    NSMutableString *newContentString = [NSMutableString string];
    va_list args;
    va_start(args, firstArg);
    for (NSString *arg = firstArg; arg != nil; arg = va_arg(args, NSString*))
    {
        [newContentString appendString:arg];
    }
    va_end(args);
    
    [contents autorelease];
    contents = [newContentString retain];
}

The va_list, va_start, va_arg and va_end are all standard C syntax for handling variable arguments. To describe them simply:

  • va_list - A pointer to a list of variable arguments.
  • va_start - Initializes a va_list to point to the first argument after the argument specified.
  • va_arg - Fetches the next argument out of the list. You must specify the type of the argument (so that va_arg knows how many bytes to extract).
  • va_end - Releases any memory held by the va_list data structure.

Generally speaking, you can use this for loop for any variable argument situation where your arguments are all the same type. Other cases are a bit trickier but far less common — I'm sure you can work out how they would work if needed.

va_list in Cocoa

A number of classes in Cocoa have methods that take variable numbers of arguments. In most cases, these classes will also have an equivalent method that takes a va_list.

We can see an example of these va_list equivalents by looking at NSString. NSString declares the class method stringWithFormat:... (which takes a variable number of arguments) and NSString also declares the instance method initWithFormat:arguments: (where the arguments parameter is a va_list) which handles the equivalent behavior of stringWithFormat:....

These va_list methods are used in the situation where your class defines a method with a variable argument list and you need to pass those variable arguments into the Cocoa method. For example, if the StringContainer class listed above declared the method:

- (void)setContentsWithFormat:(NSString *)formatString, ...;

The implementation of this method would be as follows:

- (void)setContentsWithFormat:(NSString *)formatString, ...
{
    [contents autorelease];

    va_list args;
    va_start(args, formatString);
    contents = [[NSString alloc] initWithFormat:formatString arguments:args];
    va_end(args);
}

The va_list parameter allows us to pass our own variable argument list to the Cocoa method so that the Cocoa method can handle the arguments.

Creating a fake va_list

The va_list methods in Cocoa are helpful if you actually have a variable argument list.

There is another situation you many encounter though: you want to use a method like -[NSString initWithFormat:arguments:] using a runtime generated array of arguments. There is no NSString format method that takes an NSArray of arguments, so how could we handle a format string at runtime?

The answer lies in how va_list works. While GCC makes it very clear that va_list is "platform specific", the reality is that on Mac and iPhone Objective-C platforms, it is simply a byte buffer containing the arguments. In fact, if you've ever used an ABI inspection tool (like class-dump-x) on a method taking a variable argument list, you'll see that it is simply a char *.

To show how we can use this knowledge, consider the following method on the StringContainer class:

- (void)setContentsWithFormat:(NSString *)formatString arguments:(NSArray *)arguments;

If we assume that all of the variable arguments required fro the formatString are objects, then we can implement this method as follows:

- (void)setContentsWithFormat:(NSString *)formatString arguments:(NSArray *)arguments
{
    [contents autorelease];

    char *argList = (char *)malloc(sizeof(NSString *) * [arguments count]);
    [arguments getObjects:(id *)argList];
    
    contents = [[NSString alloc] initWithFormat:formatString arguments:argList];
    
    free(argList);
}

What I've done here is simply copied the NSArray to a C-style byte buffer and then passed that buffer to the initWithFormat:arguments: method.

If your arguments are not all Objective-C objects, you'd need to take greater care to assemble the byte buffer but the principles would be the same.

Some variadic notes

NSInvcation incompatibility

NSInvocation does not support variadic methods. I'm not sure why this is — maybe NSInvocation wants to avoid making assumptions about va_list, maybe it is because NSMethodSignature can't describe the storage requirements of variable arguments for NSInvocation or maybe the Cocoa developers have families and just wanted to go home early.

Variadic macros

You can also write variadic macros, much like variadic methods, by using the ##__VA_ARGS__ placeholder in your macros. See the macros near the bottom of my post titled Supersequent Implementation for an example of how this works. As discussed briefly in that article, the version of GCC used for iPhone development handles variadic macros slightly differently, so pay attention to the differences when targetting the iPhone platform.

Conclusion

Variadic methods require a degree more care than regular methods because variable argument lists have no "introspection" (you cannot ask for the number and type of arguments) — so their usage relies on documentation and implicit agreements between the sender and receiver.

In your own code, variadic methods should be used sparingly — passing variables in an NSArray or NSDictionary is safer (if slightly slower and syntactically more verbose) due to the fact that these classes do offer introspection.

When the implicit sender/receiver agreement is clear, variadic methods work well. They certainly make creating instances of NSArray, NSSet, NSDictionary easier and they are the only way to create a formatted NSString in a single invocation.

10 comments:

Matt Gallagher said...

Thanks a lot.

Matt Gallagher said...

Hello all,

I am trying to create a variable arguments list routine for NSString and then inside that routine pass that args to another routine that has a sentinel. I created my routine with ... otherButtonsTitles:(NSString*)otherTitles, ... NS_REQUIRES_NIL_TERMINATION; in the declaration and then in the implementation ... otherButtonTitles:(NSString*)otherTitles, ... {

va_list args;
va_start(args, otherTitles);

UIActionSheet* actionSheet = [[UIActionSheet alloc] initWithTitle:title
delegate:delegate
cancelButtonTitle:cancelTitle
destructiveTitle:nil
otherButtonTitles:args];
va_end(args);

...
}

this routine complains that it needs a sentinel but I am passing in [@"Yes, @"No", nil] to this routine. And when I run it, it fails with EXEC_BAD_ACCESS.

Any ideas as to why this doesn't work?

Thanks,

Kenneth

Matt Gallagher said...

Thanks for the excellent write up. I'm using it in a debug logging class I've created and it works quite well.

I haven't been writing objective-c programs for too long, but the one thing about Apple that drives me crazy is how they seem to deprecate methods, without providing a good alternative. As of Snow Leopard, this has happened to the getObjects method, because it could cause buffer overruns. (They don't trust app developers to make sure the buffer is large enough to hold the object pointers?)

Dmitry Chestnykh: "Why is that? It's a pointer, and pointers are 8 bytes on x86_64, be it NSString * or char * or whatever *."

I didn't believe it myself, so I wrote a little test program (XCode 3.2.2 on 10.6.3) and, making sure it was compiling 64 bit, I ran the test which printed the sizes of (char *) (NSString *) and (char) to terminal. The results were, as expected, 8, 8 and 1 byte(s). I'm fairly new to Apple development, so perhaps before OS X had complete 64 bit support this may have been true? (Maybe 64 bit addressing was confined to call and jmp instructions while using 32 bits for data addressing?)

Anyway, thanks for a great article!

Matt Gallagher said...

You probably can't. Try to create nil button. And then add button manually.

Matt Gallagher said...

Thank you for the excellent tutorial!

Matt Gallagher said...

here's my code for a UIAlertView subclass, which is the same:

- (id)initWithTitle:title
message:message
delegate:delegate
cancelButtonTitle:cancelButtonTitle
otherButtonTitles:otherButtonTitles, ...
{
self = [super initWithTitle:title
message:message
delegate:delegate
cancelButtonTitle:cancelButtonTitle
otherButtonTitles:nil];
va_list args;
va_start(args, otherButtonTitles);
for (NSString *arg = otherButtonTitles; arg != nil; arg = va_arg(args, NSString*)) {
[self addButtonWithTitle:arg];
}
va_end(args);
// do something...
return self;
}

Matt Gallagher said...

Best blog ever! Thank you for this great job!

Matt Gallagher said...

Perfect ... I wish more of Apple's doc was to the point as this was.

Matt Gallagher said...

Awesome articles, it was just what I was looking for. I've used your technique to override the NSString class to allow for nillable formate args to remove that pesky "(null)" value from formatted strings.

@implementation NSString(Extras)
//
+(NSString *)emptyString{

return @"";
}

//
+(NSString *)stringWithNillableFormat:(NSString *)format, ...{

va_list args;
va_start(args, format);

NSString *formattedString = [[[NSString alloc] initWithFormat:format arguments:args] stringByReplacingOccurrencesOfString:@"(null)"
withString:[NSString emptyString]];
va_end(args);

return formattedString;
}
@end


Great post!

Matt Gallagher said...

Been having difficulty getting this to support ARC. Anyone been successful?