分配块指针:Objective-C与C++类之间的差异

Jea*_*let 4 pointers subclass clang objective-c++ objective-c-blocks

我发现分配块的行为与Objective-C类参数和C++类参数的行为不同.

想象一下,我有这个简单的Objective-C类层次结构:

@interface Fruit : NSObject
@end

@interface Apple : Fruit
@end
Run Code Online (Sandbox Code Playgroud)

然后我可以写这样的东西:

Fruit *(^getFruit)();
Apple *(^getApple)();
getFruit = getApple;
Run Code Online (Sandbox Code Playgroud)

这意味着,对于Objective-C类,块在它们的返回类型中协变的:返回更具体的东西的块可以被视为块的"子类",返回更通用的东西.在这里,getApple传送苹果的getFruit块可以安全地分配给块.事实上,如果以后使用,它总是可以保存,以便Apple *在您预期时获得Fruit *.并且,从逻辑上讲,反过来不起作用:getApple = getFruit;不编译,因为当我们真的想要一个苹果时,我们不高兴得到一个水果.

同样,我可以这样写:

void (^eatFruit)(Fruit *);
void (^eatApple)(Apple *);
eatApple = eatFruit;
Run Code Online (Sandbox Code Playgroud)

这表明块在其参数类型中协变的:可以使用可以处理更通用的参数的块,其中需要处理更具体的参数的块.如果一个街区知道如何吃水果,它也会知道怎么吃苹果.同样,反之亦然,这不会编译:eatFruit = eatApple;.

在Objective-C中,这一切都很好.现在让我们尝试使用C++或Objective-C++,假设我们有类似的C++类:

class FruitCpp {};

class AppleCpp : public FruitCpp {};

class OrangeCpp : public FruitCpp {};
Run Code Online (Sandbox Code Playgroud)

遗憾的是,这些块分配不再编译:

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

 void (^eatFruitCpp)(FruitCpp *);
 void (^eatAppleCpp)(AppleCpp *);
 eatAppleCpp = eatFruitCpp; // error!
Run Code Online (Sandbox Code Playgroud)

Clang抱怨"从不兼容的类型中分配"错误.因此,对于C++类,块在返回类型和参数类型中看起来是不变的.

这是为什么?我用Objective-C类创建的相同参数是否也适用于C++类?我错过了什么?

小智 11

由于Objective-C和C++对象模型之间的差异,这种区分是有意的.特别是,给定指向Objective-C对象的指针,可以将该指针转换/转换为指向基类或派生类,而不实际更改指针的值:对象的地址无论如何都是相同的.

因为C++允许多个和虚拟继承,所以C++对象不是这样的:如果我有一个指向C++类的指针,并且我将该指针转换/转换为指向基类或派生类,我可能需要调整指针的值.例如,考虑:

class A { int x; }
class B { int y; }
class C : public A, public B { }

B *getC() { 
  C *c = new C;
  return c;
}
Run Code Online (Sandbox Code Playgroud)

假设getC()中的新C对象在地址0x10处分配.指针'c'的值是0x10.在return语句中,需要调整指向C的指针以指向C中的B子对象.因为B在C的继承列表中位于A之后,它(通常)将在A之后布置在内存中,所以这意味着添加一个指向4个字节(== sizeof(A))的偏移量,因此返回的指针将为0x14.类似地,将B*转换为C*将从指针中减去4个字节,以考虑B中B的偏移.在处理虚拟基类时,想法是相同的但是不再知道偏移,编译时常量:在执行期间通过vtable访问它们.

现在,考虑这对分配的影响,如:

C (^getC)();
B (^getB)();
getB = getC;
Run Code Online (Sandbox Code Playgroud)

getC块返回指向C的指针.要将其转换为返回指向B的指针的块,我们需要通过添加4个字节来调整从块的每次调用返回的指针.这不是对块的调整; 它是对块返回的指针值的调整.可以通过合成包裹前一个块并执行调整的新块来实现这一点,例如,

getB = ^B() { return getC() }
Run Code Online (Sandbox Code Playgroud)

这在编译器中是可实现的,当用一个需要调整的协变返回类型的虚函数覆盖虚函数时,它已经引入了类似的"thunks".但是,使用块会导致另外一个问题:块允许与==进行相等比较,因此要评估"getB == getC",我们必须能够查看由赋值"getB ="生成的thunk. getC"比较底层块指针.同样,这是可实现的,但需要更重量级的块运行时,能够创建(单一)thunks能够对返回值(以及任何逆变参数)执行这些调整.虽然所有这些在技术上都是可行的,但成本(运行时大小,复杂性和执行时间)超过了收益.

回到Objective-C,单继承对象模型永远不需要对对象指针进行任何调整:无论指针的静态类型如何,只有一个地址指向给定的Objective-C对象,因此协方差/逆变永远不需要任何thunk,块分配是一个简单的指针赋值(ARC下的+ _Block_copy/_Block_release).