4

我发现分配块对于 Objective-C 类参数和 C++ 类参数的行为不同。

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

@interface Fruit : NSObject
@end

@interface Apple : Fruit
@end

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

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

这意味着,对于 Objective-C 类,块的返回类型是协变的:返回更具体的块可以看作是返回更一般的块的“子类”。在这里,getApple可以将传递苹果的区块安全地分配给getFruit区块。事实上,如果以后使用,Apple *当您期待Fruit *. 而且,从逻辑上讲,反过来是行不通的:getApple = getFruit;不编译,因为当我们真的想要一个苹果时,我们不高兴只得到一个水果。

同样,我可以这样写:

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

这表明块的参数类型是协变的:可以处理更一般的参数的块可以用于需要处理更具体的参数的块。如果一个块知道如何吃水果,它也会知道如何吃苹果。同样,反之亦然,这将无法编译:eatFruit = eatApple;.

这一切都很好——在 Objective-C 中。现在让我们在 C++ 或 Objective-C++ 中尝试一下,假设我们有这些类似的 C++ 类:

class FruitCpp {};

class AppleCpp : public FruitCpp {};

class OrangeCpp : public FruitCpp {};

可悲的是,这些块分配不再编译:

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

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

Clang 抱怨“从不兼容的类型分配”错误。因此,对于 C++ 类,块在返回类型和参数类型方面似乎是不变的。

这是为什么?我对 Objective-C 类提出的相同论点不也适用于 C++ 类吗?我错过了什么?

4

2 回答 2

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;
}

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

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

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

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

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

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

回到 Objective-C,单继承对象模型永远不需要对对象指针进行任何调整:只有一个地址指向给定的 Objective-C 对象,而不管指针的静态类型如何,所以协变/逆变从不需要任何 thunk,块分配是一个简单的指针分配(ARC 下的 + _Block_copy/_Block_release)。

于 2013-03-11T14:32:32.983 回答
1

该功能可能被忽略了。有些提交表明 Clang 人关心在 Objective-C++ 中为 Objective-C 类型提供协变和逆变工作,但我找不到 C++ 本身的任何东西。块的语言规范没有提到 C++ 或 Objective-C 的协变或逆变。

于 2013-03-10T21:36:44.753 回答