Block Pointer Assignment: Differences Between Objective-C vs C ++ Classes

Ive found that block assignment behaves differently with respect to Objective-C class parameters and C ++ class parameters.

Imagine that I have this simple hierarchy of Objective-C classes:

@interface Fruit : NSObject @end @interface Apple : Fruit @end 

Then I can write the following:

 Fruit *(^getFruit)(); Apple *(^getApple)(); getFruit = getApple; 

This means that with respect to Objective-C classes, blocks are covariant in their inverse type : a block that returns something more specific can be thought of as a “subclass” of a block that returns something more general. Here, the getApple block that delivers the apple can be safely assigned to the getFruit block. Indeed, if it is used later, it always saves to get Apple * when you expect Fruit * . And, logically, the opposite does not work: getApple = getFruit; It doesn’t compile, because when we really want an apple, we don’t like to get only the fruit.

Similarly, I can write this:

 void (^eatFruit)(Fruit *); void (^eatApple)(Apple *); eatApple = eatFruit; 

This shows that blocks are covariant in their types of arguments : a block that can handle a more general general argument can be used where a block is needed that processes a more specific argument. If a block knows how to eat fruit, he will know how to eat an apple. Again, the converse is not true, and this will not compile: eatFruit = eatApple; .

All this is good and good - in Objective-C. Now try this in C ++ or Objective-C ++, suppose we have similar C ++ classes:

 class FruitCpp {}; class AppleCpp : public FruitCpp {}; class OrangeCpp : public FruitCpp {}; 

Unfortunately, these block assignments no longer compile:

  FruitCpp *(^getFruitCpp)(); AppleCpp *(^getAppleCpp)(); getFruitCpp = getAppleCpp; // error! void (^eatFruitCpp)(FruitCpp *); void (^eatAppleCpp)(AppleCpp *); eatAppleCpp = eatFruitCpp; // error! 

Clang complains about the "assign from incompatible type" error. Thus, with respect to C ++ classes, the blocks seem invariant in terms of the type of the return value and the types of parameters .

Why? Isn't the same argument that I made with Objective-C classes appropriate for C ++ classes? What am I missing?

+4
source share
2 answers

This distinction is deliberate due to differences between Objective-C and C ++ object models. In particular, taking into account the pointer to the Objective-C object, you can convert / drop this pointer to the base class or derived class without actually changing the value of the pointer: the address of the object is the same independently.

Since C ++ allows multiple and virtual inheritance, this does not apply to C ++ objects: if I have a pointer to a C ++ class and I pass / convert this pointer to a base class or derived class, I may have to configure pointer values. For example, consider:

 class A { int x; } class B { int y; } class C : public A, public B { } B *getC() { C *c = new C; return c; } 

Let's say that the new C object in getC () is allocated at 0x10. The value of the 'c' pointer is 0x10. In the return statement, this pointer to C needs to be adjusted to point to the subobject B inside C. Since B comes after the inheritance list of A in C, it will (usually) be laid out after A, so this means adding an offset of 4 bytes (= = sizeof (A)) to the pointer, so the returned pointer will be 0x14. Similarly, casting B * to C * would subtract 4 bytes from the pointer to account for the offset B inside C. When it comes to virtual base classes, the idea is the same, but the offsets are no longer known, compile-time constants: at runtime, they access through vtable.

Now consider the effect this has on assignment, for example:

 C (^getC)(); B (^getB)(); getB = getC; 

The getC block returns a pointer to C. To turn it into a block that returns a pointer to B, we will need to adjust the pointer returned from each call to the block by adding 4 bytes. This is not a block adjustment; this is an adjustment to the value of the pointer returned by the block. This can be realized by synthesizing a new block, which wraps the previous block and performs tuning, for example,

 getB = ^B() { return getC() } 

This is implemented in a compiler that already introduces similar "thunks" when overriding a virtual function with a function that requires a covariant return type that needs to be configured. However, an additional problem arises with blocks: blocks allow you to compare equality with ==, so to evaluate "getB == getC" we would need to be able to look at the thunk that would be generated by assigning "getB = getC" to compare the base block pointers. Again, this can be implemented, but it will require much heavier runtime blocks, capable of creating (uniqued) thunks, capable of performing these adjustments to the return value (as well as for any contravariant parameters). Although all this is technically possible, the cost (in size, complexity and lead time) outweighs the benefits.

Returning to Objective-C, an object model with a single inheritance never needs any adjustments to the object pointer: only one address points to this Objective-C object, regardless of the static type of the pointer, so covariance / contravariance never requires any tricks and block assignment is a simple pointer assignment (+ _Block_copy / _Block_release with ARC).

+10
source

the function was probably missed. There are commits that show that Clang people make sure that covariance and contravariance work in Objective-C ++ for Objective-C types, but I couldn't find anything for C ++ itself. The language specification for blocks does not contain covariance or contravariance for C ++ or Objective-C.

+1
source

All Articles