访问作为“inout”传递的“var”是未定义的行为?

Accessing a `var` passed as `inout` is undefined behavior?

提问人:lcmylin 提问时间:9/14/2017 最后编辑:Communitylcmylin 更新时间:9/14/2017 访问量:226

问:

inout 参数的文档中:

输入输出参数传递如下:

  1. 调用函数时,将复制参数的值。
  2. 在函数的主体中,副本被修改。
  3. 当函数返回时,副本的值将分配给原始参数。

仅从这个描述中,我假设在作为参数传递的范围内修改作为参数传递的变量是没有意义的,因为原始变量保证在调用返回后被覆盖。举个人为的例子:inoutinout

var x: Int = 5

({ (inoutX: inout Int) in
    inoutX = 7
    x = 6
})(&x)

print(x) // Expecting "7"

原始变量可通过变异捕获进行访问,因此仍可分配给该变量。预期的打印输出为“7”,因为这是函数调用结束时的值。但是如果我在 Swift 4 REPL 中运行它,我实际上会得到“6”!xinoutX

该文档对此行为进行了一些说明:

作为优化,当参数是存储在内存中物理地址的值时,函数体内部和外部都使用相同的内存位置。

但随后又有一句非常明显不准确的声明:

优化的行为称为引用调用;它满足了复制-传入复制模型的所有要求,同时消除了复制的开销。

显然,优化后的行为不符合参数声称符合的按值结果调用约定。然后,文档承认了这一点,但反过来,解释了为什么您不应该依赖引用调用行为:inout

使用复制-传入-复制输出给出的模型编写代码,而不依赖于按引用调用优化,以便无论是否进行优化,它都能正常运行。

不要访问作为 in-out 参数传递的值,即使原始参数在当前作用域中可用。当函数返回时,您对原始内容所做的更改将被副本的值覆盖。不要依赖按引用调用优化的实现来尝试防止更改被覆盖。

那么我可以收集到的是,参数是按值结果调用的,除非它们是按引用调用的。而且,根据文档不希望您依赖引用调用语义的程度,我只能猜测优化不是在一组定义明确的情况下执行的。如果是这样的话,那么我只能得出结论,在输入范围内访问作为 inout 参数传递的变量是未定义的行为。inout

这是一个相对不幸的结论,我对文档的不情愿感到困惑。为什么它会试图将参数表示为遵循特定的调用约定,而(除了 setter 或属性观察者之外)这些调用约定的语义无法以定义的方式观察到?令人困惑的是,我在这里怀疑自己的结论,因此问题来了:我的理解正确吗?inout

Swift 按引用传递 语义 Swift4 Inout

评论

1赞 Martin R 9/14/2017
代码调用未定义的行为。在 Swift 4 (Xcode 9 GM) 中,它中止并显示错误消息Simultaneous accesses to 0x1005e3ec0, but modification requires exclusive access.
1赞 Hamish 9/14/2017
从 Swift 3 开始,参数总是被编译为按引用传递的,这解释了你所看到的行为。但是,它们(通常)具有复制-输入-复制输出的语义,这就是您应该如何处理它们(例如,不能保证对传递的变量的引用在调用结束后仍然有效)。您的代码是 Swift 4 中引入独占访问法的一个完美例子;它阻止了编写此类难以推理的代码。正如 Martin 所说,动态实施将在运行时捕获这种重叠的访问并捕获陷阱。inout
1赞 Hamish 9/15/2017
@MartinR 尽管它不再是未定义的行为,但鉴于运行时强制执行可以防止它发生:)以前是否是未定义的行为是一个有趣的问题;我实际上不太相信它是,因为 SE-0176 的动机之一是“因此,由于重叠,编译器必须悲观地假设这些方法调用中的每一个都可能以某种方式找到一种方法来修改 set 绑定到的原始变量”并且......
1赞 Hamish 9/15/2017
所有权宣言中可以看出:“今天的 Swift 允许嵌套访问同一变量;例如,可以将单个变量作为两个不同的 inout 参数传递,或者可以向某个方法传递一个回调,该回调以某种方式访问调用该方法的同一变量。这样做大多是不鼓励的,但这不是被禁止的,编译器和标准库都必须向后弯腰,以确保程序在发生这种情况时不会表现得太糟糕。....
1赞 Hamish 9/15/2017
因此,我猜想,由于编译器做出了这些悲观的假设,它很可能是定义(但未指定)的行为。话虽如此,它确实违反了 的复制-进复制-输出语义。inout

答: 暂无答案