How do I safely cast a `_Nullable` to a `_Nonnull` in Objective-C?

10.3k Views Asked by At

When compiling with -Wnullable-to-nonnull-conversion, we get a proper warning with the following code:

NSString * _Nullable maybeFoo = @"foo";
^(NSString * _Nonnull bar) {  
}(maybeFoo);

Tests.m:32:7: error: implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull' [-Werror,-Wnullable-to-nonnull-conversion]
    }(maybeFoo);
      ^
1 error generated.

How do I safely cast foo from an NSString * _Nullable to an NSString * _Nonnull?

The best solution I have so far

The best I've come up with is this macro:

#define ForceUnwrap(type, nullableExpression) ^type _Nonnull () { \
  type _Nullable maybeValue___ = nullableExpression; \
  if (maybeValue___) { \
    return (type _Nonnull) maybeValue___; \
  } else { \
    NSLog(@"Attempted to force unwrap a null: " #nullableExpression); \
    abort(); \
  } \
}()

Which is used like:

NSString * _Nullable maybeFoo = @"foo";
if (maybeFoo) {
    NSString * _Nonnull foo = ForceUnwrap(NSString *, maybeFoo);
    ^(NSString * _Nonnull bar) {
    }(foo);
}

And which produces an error if assigned to a wrongly-typed variable:

NSString * _Nullable maybeFoo = @"foo";
if (maybeFoo) {
    NSNumber * _Nonnull foo = ForceUnwrap(NSString *, maybeFoo);
    ^(NSNumber * _Nonnull bar) {
    }(foo);
}

Tests.m:40:29: error: incompatible pointer types initializing 'NSNumber * _Nonnull' with an expression of type 'NSString * _Nonnull' [-Werror,-Wincompatible-pointer-types]
        NSNumber * _Nonnull foo = ForceUnwrap(NSString *, maybeFoo);
                            ^     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

And which produces an error if cast to the wrong type:

NSString * _Nullable maybeFoo = @"foo";
if (maybeFoo) {
    NSNumber * _Nonnull foo = ForceUnwrap(NSNumber *, maybeFoo);
    ^(NSNumber * _Nonnull bar) {
    }(foo);
}

Tests.m:40:35: error: incompatible pointer types initializing 'NSNumber * _Nullable' with an expression of type 'NSString * _Nullable' [-Werror,-Wincompatible-pointer-types]
        NSNumber * _Nonnull foo = ForceUnwrap(NSNumber *, maybeFoo);
                                  ^                       ~~~~~~~~
Tests.m:27:16: note: expanded from macro 'ForceUnwrap'
type _Nullable maybeValue___ = nullableExpression; \
               ^               ~~~~~~~~~~~~~~~~~~
1 error generated.

Unfortunately, if you need to cast to a generic type with multiple arguments, you have to resort to preprocessor hacks:

NSDictionary<NSString *, NSString *> * _Nullable maybeFoo = 
[NSDictionary<NSString *, NSString *> new];
if (maybeFoo) {
  NSDictionary<NSString *, NSString *> * _Nonnull foo =
#define COMMA ,
  ForceUnwrap(NSDictionary<NSString * COMMMA NSString *>, maybeFoo);
#undef COMMA
  ^(NSDictionary<NSString *, NSString *> * _Nonnull bar) {
  }(foo);
}

Things I've tried that don't work

Assigning maybeFoo directly to an NSString * _Nonnull doesn't work. It produces the same error as before:

NSString * _Nullable maybeFoo = @"foo";
if (maybeFoo) {
  NSString * _Nonnull foo = maybeFoo;
  ^(NSString * _Nonnull bar) {  
  }(foo);
}

Tests.m:30:35: error: implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull' [-Werror,-Wnullable-to-nonnull-conversion]
        NSString * _Nonnull foo = maybeFoo;
                                  ^
1 error generated.

And casting to maybeFoo to NSString * _Nonnull isn't safe because if maybeFoo's type changes, the compiler won't break:

NSNumber * _Nullable maybeFoo = @"foo";
if (maybeFoo) {
  NSString * _Nonnull foo = (NSString * _Nonnull) maybeFoo;
  ^(NSString * _Nonnull bar) {  
  }(foo);
}
// no errors!

I also tried using __typeof__, when casting, but __typeof__ carries the nullability specifier, so when you try to cast to __typeof__(maybeFoo) _Nonnull you get a nullability conflict:

NSString * _Nullable maybeFoo = @"foo";
if (maybeFoo) {
    NSString * _Nonnull foo = (__typeof__(maybeFoo) _Nonnull) maybeFoo;
    ^(NSString * _Nonnull bar) {
    }(foo);
}

Tests.m:30:57: error: nullability specifier '_Nonnull' conflicts with existing specifier '_Nullable'
        NSString * _Nonnull foo = (__typeof__(maybeFoo) _Nonnull) maybeFoo;
                                                        ^
Tests.m:30:35: error: implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull' [-Werror,-Wnullable-to-nonnull-conversion]
        NSString * _Nonnull foo = (__typeof__(maybeFoo) _Nonnull) maybeFoo;
                                  ^
2 errors generated.

Everything was run with the deep static analyzer and compiled with Xcode 8.2.1 with the following flags:

-Wnon-modular-include-in-framework-module 
-Werror=non-modular-include-in-framework-module
-Wno-trigraphs
-Werror
-Wno-missing-field-initializers
-Wno-missing-prototypes
-Wunreachable-code
-Wno-implicit-atomic-properties
-Wno-arc-repeated-use-of-weak
-Wduplicate-method-match
-Wno-missing-braces
-Wparentheses
-Wswitch
-Wunused-function
-Wno-unused-label
-Wno-unused-parameter
-Wunused-variable
-Wunused-value
-Wempty-body
-Wuninitialized
-Wno-unknown-pragmas
-Wno-shadow
-Wno-four-char-constants
-Wno-conversion
-Wconstant-conversion
-Wint-conversion
-Wbool-conversion
-Wenum-conversion
-Wshorten-64-to-32
-Wpointer-sign
-Wno-newline-eof
-Wno-selector
-Wno-strict-selector-match
-Wundeclared-selector
-Wno-deprecated-implementations
-Wno-sign-conversion
-Wno-infinite-recursion
-Weverything
-Wno-auto-import
-Wno-objc-missing-property-synthesis
-Wno-cstring-format-directive
-Wno-direct-ivar-access
-Wno-double-promotion
3

There are 3 best solutions below

1
On BEST ANSWER

The best I found so far is a trick with generics.

Essentially you define an interface that uses generics and has a method that returns the generic type as nonnull. Then in your macro you use typeof but on the generic type, and this gives you the correct type.

Note that the generic class is never instantiated, it's just used to get the correct type.

@interface RBBBox<__covariant Type>

- (nonnull Type)asNonNull;

@end

#define RBBNotNil(V) \
    ({ \
        NSCAssert(V, @"Expected '%@' not to be nil.", @#V); \
        RBBBox<__typeof(V)> *type; \
        (__typeof(type.asNonNull))V; \
    })

This is not my idea, though. Source: https://gist.github.com/robb/d55b72d62d32deaee5fa

1
On

Michael Ochs' answer was basically correct, but I've since run into some static analyzer warnings because of lack of hard _Nonnull guarantees within. In short, we must abort if we receive a nil or else when we do an assignment like this:

@interface Foo : NSObject
+ (NSString * _Nullable)bar;
@end

int main(int argc, char * argv[]) {
  NSString * _Nonnull bar = RBBNotNil([Foo bar]);
}

In a Release configuration (in my case, when Archiving), the static analyzer will complain that you're attempting to assign a _Nullable value to a _Nonnull lvalue. I received warnings like this:

nil assigned to a pointer which is expected to have non-null value

This is my updated version:

// We purposefully don't have a matching @implementation.
// We don't want +asNonnull to ever actually be called
// because that will add a lot of overhead to every RBBNotNil
// and we want RBBNotNil to be very cheap.
// If there is no @implementation, then if the +asNonnull is
// actually called, we'll get a linker error complaining about
// the lack of @implementation.
@interface RBBBox <__covariant Type>

// This as a class method so you don't need to
// declare an unused lvalue just for a __typeof
+ (Type _Nonnull)asNonnull;

@end

/*!
 * @define RBBNotNil(V)
 * Converts an Objective-C object expression from _Nullable to _Nonnull. 
 * Crashes if it receives a nil! We must crash or else we'll receive
 * static analyzer warnings when archiving. I think in Release mode,
 * the compiler ignores the _Nonnull cast.
 * @param V a _Nullable Objective-C object expression
 */
#define RBBNotNil(V) \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wgnu-statement-expression\"") \
({ \
__typeof__(V) __nullableV = V; \
NSCAssert(__nullableV, @"Expected '%@' not to be nil.", @#V); \
if (!__nullableV) { \
    abort(); \
} \
(__typeof([RBBNotNil<__typeof(V)> asNonnull]))__nullableV; \
}) \
_Pragma("clang diagnostic pop")
0
On

I use this macro:

#define assumeNotNull(_value)         \
({                                    \
    if (!_value) abort();             \
    __auto_type const _temp = _value; \
    _temp;                            \
})

Of course, only after an appropriate test in code:

if (parameters) {
    [obj processParameters:assumeNotNull(parameters)];
}

Leaving out the macro the compiler would tell me that parameters might be NULL but processParameters requires a non-NULL argument. In my case that is even configured to be an error, not just a warning.

Leaving out the if check, the code will compile but if I ever feed in NULL, the application will crash on spot. So one should only use the macro after a test or if one is absolutely sure that the value cannot be NULL for some reason and you are so sure about that, that you are willing to bet your app stability on it.

If in doubt, always test and keep in mind, that if a test is clearly unnecessary (e.g. the condition was tested before and the code will never be reached if the value was NULL), the compiler will detect that during the optimization phase and remove the test for you. Unnecessary testing is hardly ever a performance problem, especially not with a test that cheap.

Update

Starting with Xcode 14.3 (LLVM 15) clang no longer understands that the if-statement ensures that _value is not NULL (after all abort() is a no-return function) and instead still throws an error. See also issue 63018.

As a workaround you can use this macro instead:

#define assumeNotNull(_value)         \
({                                    \
    if (!_value) abort();             \
    __auto_type const _temp = _value; \
    (typeof(*_temp) *_Nonnull)_temp;  \
})

Works okay for most cases but will not work with blocks as you cannot de-reference blocks. I still hope to find a better workaround.

The reason why this has changed could be an old radar: http://www.openradar.me/36877120

Also two month ago someone already complained about this change on the Apple developer forum: https://developer.apple.com/forums/thread/726000