多次调用 Variadic Obj-C 方法导致出现无效参数
Calling variadic Obj-C method multiple times causes invalid arguments to appear
我有一个可变参数方法,我想将多个枚举值传递给:
typedef NS_ENUM(NSInteger, Enum) {
Enum0 = 0,
Enum1 = 1,
Enum2 = 2,
Enum3 = 3,
Enum4 = 4,
Enum5 = 5,
Enum6 = 6,
};
@interface Variadics : NSObject
- (void)test:(Enum)enm, ...;
@end
这是实现
@implementation Variadics
- (void)test:(Enum)enm, ... {
va_list args;
va_start(args, enm);
for (; enm != 0; enm = va_arg(args, Enum))
{
NSAssert(enm <= Enum6, @"invalid enum");
}
va_end(args);
}
@end
我是这样称呼它的:
Variadics* var = [[Variadics alloc] init];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
效果很好!但是如果我把它改成这样:
Variadics* var = [[Variadics alloc] init];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
突然间我想到了那个断言 enum == 4294967296
。更奇怪的是,我在第一次调用 时就命中了断言 。后来的三个电话从来没有 运行.
这是怎么回事?
您在参数列表末尾的零终止作为 int
(32 位)值传递给您的函数,但您的 va_arg
正在拉出一个 NSInteger
(又名 long
)来自堆栈的值。因此,您从堆栈中吸取了额外的 32 位,并将其全部视为一个 64 位值,其中一半是您想要的零,另一半是内存中与它相邻的任何值那个时候。
您必须这样做才能获得您想要的行为:
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, (NSInteger)0];
正如 Rob Mayoff 在下面的评论中阐明的那样,作为整数值的未转换的零文字被视为 int
。适用普通 C 整数提升规则*;在可变参数列表中,因为参数没有声明类型,较小的整数类型被提升为--并作为--int
s传递。因为编译器无法看到您希望在运行时看到的实际类型,所以 int
不会为您提升为 long
,因此在堆栈中作为 int
结束.
通常,varargs 终止是隐式完成的(printf
arg 数量和类型的样式预知)或使用像 nil
这样的常量,这将是正确的宽度,这些方法避免了这个问题.在旧的 32 位世界中,枚举值是 int
s, 和 NSInteger
是 int
,默认整数提升是 还有到int
,这些区别被隐藏了。
实际上,这对您的代码设计可能意味着您可能会保留一个特殊的哨兵枚举值(不一定为零)用作列表终止符以保证它是正确的类型。或者您可能会修改以在函数调用前添加参数数量。
*参见 C18 标准 §6.3.1.1 ;-)
红利解说:为什么看到值enum == 4294967296
?
小数4294967296等于0x0000000100000000
。第二个(下)一半是你放在那里的 32 位零。上半部分看起来只是数字 1
。起初我假设这将是当前堆栈帧的其他(有效)部分,但一些调查(在 64 位 Mac 使用当前 llvm 和 Xcode 等)表明编译器通过使用 movq
(移动四边形 =64 位)推送枚举参数和使用 movl
(移动长 =32 位)推送最终零参数来设置对 -test:...
的调用。因为堆栈是 64 位对齐的,所以在倒数第二个和终端参数之间的堆栈内存中留下了 32 位的 "hole"(即,未被覆盖)。那是包含 0x1
的位置。所以你不是在阅读相邻的参数,或者其他有效但用于其他用途的值。您正在从某个早期函数调用的工作区中读取幻影值。在我的调试中,它似乎不是来自 Variable
的 alloc/init -- 它是先于手头的测试代码且无关的东西。
我有一个可变参数方法,我想将多个枚举值传递给:
typedef NS_ENUM(NSInteger, Enum) {
Enum0 = 0,
Enum1 = 1,
Enum2 = 2,
Enum3 = 3,
Enum4 = 4,
Enum5 = 5,
Enum6 = 6,
};
@interface Variadics : NSObject
- (void)test:(Enum)enm, ...;
@end
这是实现
@implementation Variadics
- (void)test:(Enum)enm, ... {
va_list args;
va_start(args, enm);
for (; enm != 0; enm = va_arg(args, Enum))
{
NSAssert(enm <= Enum6, @"invalid enum");
}
va_end(args);
}
@end
我是这样称呼它的:
Variadics* var = [[Variadics alloc] init];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
效果很好!但是如果我把它改成这样:
Variadics* var = [[Variadics alloc] init];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
突然间我想到了那个断言 enum == 4294967296
。更奇怪的是,我在第一次调用 时就命中了断言 。后来的三个电话从来没有 运行.
这是怎么回事?
您在参数列表末尾的零终止作为 int
(32 位)值传递给您的函数,但您的 va_arg
正在拉出一个 NSInteger
(又名 long
)来自堆栈的值。因此,您从堆栈中吸取了额外的 32 位,并将其全部视为一个 64 位值,其中一半是您想要的零,另一半是内存中与它相邻的任何值那个时候。
您必须这样做才能获得您想要的行为:
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, (NSInteger)0];
正如 Rob Mayoff 在下面的评论中阐明的那样,作为整数值的未转换的零文字被视为 int
。适用普通 C 整数提升规则*;在可变参数列表中,因为参数没有声明类型,较小的整数类型被提升为--并作为--int
s传递。因为编译器无法看到您希望在运行时看到的实际类型,所以 int
不会为您提升为 long
,因此在堆栈中作为 int
结束.
通常,varargs 终止是隐式完成的(printf
arg 数量和类型的样式预知)或使用像 nil
这样的常量,这将是正确的宽度,这些方法避免了这个问题.在旧的 32 位世界中,枚举值是 int
s, 和 NSInteger
是 int
,默认整数提升是 还有到int
,这些区别被隐藏了。
实际上,这对您的代码设计可能意味着您可能会保留一个特殊的哨兵枚举值(不一定为零)用作列表终止符以保证它是正确的类型。或者您可能会修改以在函数调用前添加参数数量。
*参见 C18 标准 §6.3.1.1 ;-)
红利解说:为什么看到值enum == 4294967296
?
小数4294967296等于0x0000000100000000
。第二个(下)一半是你放在那里的 32 位零。上半部分看起来只是数字 1
。起初我假设这将是当前堆栈帧的其他(有效)部分,但一些调查(在 64 位 Mac 使用当前 llvm 和 Xcode 等)表明编译器通过使用 movq
(移动四边形 =64 位)推送枚举参数和使用 movl
(移动长 =32 位)推送最终零参数来设置对 -test:...
的调用。因为堆栈是 64 位对齐的,所以在倒数第二个和终端参数之间的堆栈内存中留下了 32 位的 "hole"(即,未被覆盖)。那是包含 0x1
的位置。所以你不是在阅读相邻的参数,或者其他有效但用于其他用途的值。您正在从某个早期函数调用的工作区中读取幻影值。在我的调试中,它似乎不是来自 Variable
的 alloc/init -- 它是先于手头的测试代码且无关的东西。