140

A NSInteger is 32 bits on 32-bit platforms, and 64 bits on 64-bit platforms. Is there a NSLog specifier that always matches the size of NSInteger?

Setup

  • Xcode 3.2.5
  • llvm 1.6 compiler (this is important; gcc doesn't do this)
  • GCC_WARN_TYPECHECK_CALLS_TO_PRINTF turned on

That's causing me some grief here:

#import <Foundation/Foundation.h>

int main (int argc, const char * argv[]) {
    @autoreleasepool {
        NSInteger i = 0;
        NSLog(@"%d", i);
    }
    return 0;
}

For 32 bit code, I need the %d specifier. But if I use the %d specifier, I get a warning when compiling for 64 bit suggesting I use %ld instead.

If I use %ld to match the 64 bit size, when compiling for 32 bit code I get a warning suggesting I use %d instead.

How do I fix both warnings at once? Is there a specifier I can use that works on either?

This also impacts [NSString stringWithFormat:] and [[NSString alloc] initWithFormat:].

Steven Fisher
  • 44,462
  • 20
  • 138
  • 192

3 Answers3

308

Updated answer:

You can make use of the z and t modifiers to handle NSInteger and NSUInteger without warnings, on all architectures.

You want to use %zd for signed, %tu for unsigned, and %tx for hex.

This information comes courtesy of Greg Parker.


Original answer:

The official recommended approach is to use %ld as your specifier, and to cast the actual argument to a long.

Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • 6
    This is definitely the way to go, but I think I might use `static inline NSIntToLong(NSInteger i) {return (long)i;}`. This avoids disabling type checking completely (i.e. if the type of i changes). – Steven Fisher Dec 10 '10 at 07:22
  • 3
    Good thinking by @steven-fisher. Avoid warning with: `static inline long NSIntToLong(NSInteger i) {return (long)i;}` – Erik Nov 03 '11 at 16:48
  • 3
    You can also create an NSNumber and log that. `NSLog(@"%@",@(mynsint));` http://stackoverflow.com/questions/20355439/nsinteger-and-nsuinteger-in-a-mixed-64bit-32bit-environment – orkoden Jan 30 '14 at 11:17
  • 2
    @KevinBallard This should not be a serious performance issue. You should not use lots of NSLog in production code anyway. If you have to log lots of stuff for some reason, do it on a separate thread. – orkoden Feb 13 '14 at 09:42
  • @orkoden: That's no excuse for suggesting a habit of using unnecessarily underperforming code. Using the correct printf token is just as easy as wrapping your number in an `NSNumber`. – Lily Ballard Feb 13 '14 at 19:28
  • @KevinBallard the problem with doing an explicit cast is, that you can run into issues when the type you cast to is incorrect. E.g. you change the type of an enum from signed to unsigned or an int to a float. All logs of that variable will be wrong because of the cast. If you use NSNumber, it will always be correct. – orkoden Feb 14 '14 at 15:25
  • @orkoden: With my updated answer, no more casts are involved. – Lily Ballard Feb 15 '14 at 20:31
  • 1
    @KevinBallard I'm concerned about the updated solution as this appears to be mere coincidence and could easily break in a future release. So far as I know, nothing ties size_t to NSInteger size in a permanent way. – Richard Brightwell Apr 15 '14 at 15:19
  • @RichardBrightwell: `size_t` and `long` are the same size (on all Apple platforms). `NSInteger` is tied to `long` (well, it's `int` on 32-bit for backwards compatibility reasons, but on 32-bit `int` and `long` are the same size). – Lily Ballard Apr 16 '14 at 16:11
  • 1
    Love the new answer. :) – Steven Fisher Aug 20 '14 at 15:12
  • Perfect solution. Since I use `NSInteger` I always cast it to long `(long)` and define the placeholder as %ld.... but its a mess to write it every time again – Alex Cio Dec 19 '14 at 10:42
  • Not to mention `(long)` style casts are inherently unsafe. If `i` is something other than the expected `NSInteger`, it will (mostly) still end up a long. – Steven Fisher Jan 26 '16 at 19:49
  • 1
    The official docs say use %ld and cast to long for NSInteger. https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Strings/Articles/formatSpecifiers.html#//apple_ref/doc/uid/TP40004265-SW1 -- anything else is depending on undocumented (or just current) behavior. – Lou Franco Apr 26 '17 at 14:19
  • @LouFranco The official docs do say that, but I don't know why. Using `%zd` or `%tu` works perfectly fine and is not relying on undocumented or "happens to work" behavior. The only reason I can think of for preferring casting to long is because on 32-bit macOS `NSInteger` and `NSUInteger` are defined in terms of `int` instead of `long`, but on all Apple 32-bit platforms `int` and `long` are the same size so that's ok. – Lily Ballard Apr 27 '17 at 18:26
  • The reason I prefer casting to long is because that is what the official docs say will definitely work and is the most likely thing that may continue working in future versions. – Lou Franco Apr 27 '17 at 20:32
  • 1
    @LouFranco I guarantee you 100% the solution in my answer will work for all future versions. What the docs say works, but that doesn't mean the docs's solution is the only thing that works. – Lily Ballard Apr 28 '17 at 22:48
  • 5
    As of Xcode 9.3 there is a warning when using NSInteger as a format argument with `%zd`: `Values of type 'NSInteger' should not be used as format arguments; add an explicit cast to 'long' instead` – Rob MacEachern Apr 09 '18 at 22:31
  • @RobMacEachern Yeah I already filed a radar about it, because that warning is flat-out wrong. – Lily Ballard Apr 10 '18 at 23:35
  • But those new flags mentioned in the tweet are not in the documentation. What gives? – Iulian Onofrei May 16 '18 at 13:22
  • @KevinBallard, But **it is** relying on undocumented behavior since none of those flags are documented. – Iulian Onofrei May 16 '18 at 13:25
  • The question was more about `NSLog` than `printf`, as the very first question states. – Iulian Onofrei May 18 '18 at 07:08
  • @IulianOnofrei `NSLog` supports everything that `printf` does. It's a strict superset. – Lily Ballard May 19 '18 at 17:27
3

The accepted answer is absolutely reasonable, it is standard conforming, and correct. The only problem is that it doesn't work anymore, which is completely Apple's fault.

The format %zd is the C/C++ standard format for size_t and ssize_t. Like NSInteger and NSUInteger, size_t and ssize_t are 32 bit on a 32 bit system, and 64 bit on a 64 bit system. And that's why printing NSInteger and NSUInteger using %zd worked.

However, NSInteger and NSUInteger are defined as "long" on a 64 bit system, and as "int" on a 32 bit system (which is 64 vs 32 bit). Today, size_t is defined on "long" on all systems, which is the same size as NSInteger (either 64 or 32 bit), but a different type. Either Apple's warnings have changed (so it doesn't allow passing the wrong type to printf, even though it has the right number of bits), or the underlying types for size_t and ssize_t have changed. I don't know which one, but %zd stopped working some time ago. There is no format today that will print NSInteger without warning on both 32 and 64 bit systems.

So the only thing you can do unfortunately: Use %ld, and cast your values from NSInteger to long, or from NSUInteger to unsigned long.

Once you don't build for 32 bit anymore, you can just use %ld, without any cast.

gnasher729
  • 51,477
  • 5
  • 75
  • 98
1

The formatters come from the standard UNIX/POSIX printf function. Use %lu for unsigned long, %ld for long, %lld for long long, and %llu for unsigned long long. Try man printf on the console, but on Mac it is incomplete. The linux manpages are more explicit http://www.manpages.info/linux/sprintf.3.html

Both warnings can only be fixed by NSLog(@"%lu", (unsigned long)arg); combined with a cast as the code will be compiled in 32 AND 64 bit for iOS. Otherwise each compilation creates a separate warning.

cat
  • 2,871
  • 1
  • 23
  • 28