Rust: 未标记的联合(RFC 1444的跟踪问题)

创建于 2016-04-08  ·  210评论  ·  资料来源: rust-lang/rust

rust-lang / rfcs#1444的跟踪问题。

未解决的问题:

  • [x]直接分配给并集字段是否会触发先前内容的删除?
  • [x]移出联合的一个字段时,其他字段是否被视为无效? ( 1234
  • []在什么情况下可以为工会实施Copy ? 例如,如果某些变体为非复制类型怎么办? 所有变体?
  • []联合与枚举布局优化之间存在什么相互作用? (https://github.com/rust-lang/rust/issues/36394)

高导入率的未解决问题:

B-RFC-approved B-unstable C-tracking-issue F-untagged_unions T-lang disposition-merge finished-final-comment-period

最有用的评论

@nrc
好吧,子集非常明显-“ FFI联合”或“ C联合”或“ C ++ 11之前的联合”-即使它不是语法。 我的最初目标是尽快稳定此子集(理想情况下,此循环),以便可以在winapi类的库中使用它。
剩余的子集及其实现没有什么特别可疑的,它不是紧急的,需要等待不确定的时间,直到“ Union 1.2” RFC的过程完成。 我的期望是在稳定初始子集后的1、2或3个周期内稳定其余部分。

所有210条评论

在关于RFC的讨论中,我可能已经错过了它,但是我认为联合变量的析构函数永远不会运行是正确的吗? 在此示例中,是否将运行Box::new(1)的析构函数?

union Foo {
    f: i32,
    g: Box<i32>,
}

let mut f = Foo { g: Box::new(1) };
f.g = Box::new(2);

@sfackler我目前的理解是f.g = Box::new(2) _will_运行析构函数,但是f = Foo { g: Box::new(2) }不会。 也就是说,分配给Box<i32>左值将像往常一样导致下降,但是分配给Foo左值将不会。

因此,对变量的赋值就像断言该字段以前是“有效的”?

@sfackler对于Drop类型,是的,这是我的理解。 如果它们先前无效,则需要使用Foo构造函数表格或ptr::write 。 从快速的grep来看,RFC似乎并没有明确说明这一细节。 我将其视为一般规则的实例化,即写入Drop左值会导致析构函数调用。

带Drop变体的&mut联合应该是一件皮毛吗?

2016年4月8日星期五,斯科特·奥尔森(Scott Olson) [email protected]写道:

@sfackler https://github.com/sfackler对于Drop类型,是的,这是我的
理解。 如果它们以前无效,则需要使用Foo
构造函数形式或ptr :: write。 从快速grep来看,似乎
RFC对此内容是明确的。

-
您收到此消息是因为您已订阅此线程。
直接回复此电子邮件或在GitHub上查看
https://github.com/rust-lang/rust/issues/32836#issuecomment -207634431

在太平洋夏令时间2016年4月8日下午3:36:22,Scott Olson [email protected]写道:

@sfackler对于Drop类型,是的,这是我的理解。 如果他们
以前不是有效的,则需要使用Foo构造函数表单,或者
ptr::write 。 从快速的grep来看,RFC似乎不是
明确地对此细节。

我应该已经明确涵盖了这种情况。 我认为这两种行为都是可以辩护的,但我认为从不隐含放弃任何领域都不足为奇。 RFC已经为实现Drop的类型的联合字段推荐了lint。 我认为分配给一个字段并不意味着该字段以前有效。

是的,这种方法对我来说似乎也没有那么危险。

分配给并集字段时不删除将使f.g = Box::new(2)行为与let p = &mut f.g; *p = Box::new(2)有所不同,因为您不能使后一种情况_not_下降。 我认为我的方法不足为奇。

这也不是一个新问题。 如果未初始化fooDrop unsafe程序员已经必须处理其他情况,其中foo = bar是UB。

我个人根本不打算将Drop类型与联合使用。 因此,我将完全奉献给使用过类似的不安全代码的语义的人。

我也不想在联合中使用Drop类型,因此只要保持一致,这两种方式对我来说都无关紧要。

我不打算使用对工会的可变引用,并且可能
只是带有“怪异”标签的

2016年4月8日星期五,彼得·阿塔希安(Peter Atashian) [email protected]写道:

我也不想在联合中使用Drop类型,因此无论哪种方式都不会
只要是一致的,对我来说就很重要。

-
您收到此消息是因为您已订阅此线程。
直接回复此电子邮件或在GitHub上查看
https://github.com/rust-lang/rust/issues/32836#issuecomment -207653168

似乎这是一个未解决的好问题。 我不确定我喜欢哪种方法。

@nikomatsakis尽管我发现分配给Drop类型的并集字段需要该字段的先前有效性是很尴尬的,但提到的参考案例@tsion几乎是不可避免的。 我认为这可能只是与代码相关的陷阱,该代码有意禁用了将带有Drop的类型放入联合中的皮棉。 (对此的简短解释应该在该皮棉的解释性文本中。)

我想重申, unsafe程序员必须已经普遍知道a = b意味着drop_in_place(&mut a); ptr::write(&mut a, b)可以编写安全的代码。 不放弃联合字段将是一个更多的例外,而不是更少的例外。

(注意:当已知a已被初始化,例如let a; a = b;时,下降不会发生。)

但是我支持针对工会中的Drop变体发出默认警告,提示人们必须#[allow(..)]因为这是一个相当明显的细节。

@tsion对于a = b不是正确的,也许有时对a.x = b才是正确的,但对于*a = b确实是正确的。 这种不确定性使我对此犹豫不决。 例如,这将编译:

fn main() {
  let mut x: (i32, i32);
  x.0 = 2;
  x.1 = 3;
}

(尽管稍后尝试打印x失败,但是我认为是一个错误)

@nikomatsakis这个例子对我来说是新的。 考虑到我以前的经验,我想我会认为该示例可以编译为一个错误。

但是我不确定我是否看到了这个例子的意义。 为什么我说的对a = b而不是对a.x = b

假设,如果x.0具有带析构函数的类型,则肯定会调用该析构函数:

fn main() {
    let mut x: (Box<i32>, i32);
    x.0 = Box::new(2); // x.0 statically know to be uninit, destructor not called
    x.0 = Box::new(3); // x.0 destructor is called before writing new value
}

也许只是反对这种写作?

我的观点只是=不会总是运行析构函数。 它
使用有关目标是否已知的一些知识
初始化。

在2016年4月12日,星期二,00-07:04:10:3​​9PM,斯科特·奥尔森(Scott Olson)写道:

@nikomatsakis这个例子对我来说是新的。 考虑到我以前的经验,我想我会认为该示例可以编译为一个错误。

但是我不确定我是否看到了这个例子的意义。 为什么我说的对a = b不正确,而对于“ ax = b”有时不正确?

假设,如果x.0具有带析构函数的类型,则肯定会调用该析构函数:

fn main() {
    let mut x: (Box<i32>, i32);
    x.0 = Box::new(2); // x.0 statically know to be uninit, destructor not called
    x.0 = Box::new(3); // x.0 destructor is called
}

@nikomatsakis

如果设置了drop标志,它将运行析构函数。

但是我认为这种写法仍然令人困惑,所以为什么不仅仅禁止它呢? 您可以随时执行*(&mut u.var) = val

我的观点只是=不会总是运行析构函数。 它使用一些有关是否已知目标已初始化的知识。

@nikomatsakis我已经提到过:

(注意:当静态已知a已经被初始化时,不会发生掉落,例如let a; a = b;。)

但是我没有考虑对丢弃标志的动态检查,因此这肯定比我考虑的要复杂。

@tsion

丢弃标志只是半动态的-归零丢弃消失后,它们是代码生成的一部分。 我说我们禁止这种写作,因为这样做的弊大于利。

工会中甚至应该允许Drop类型吗? 如果我对事情的理解正确,那么在Rust中拥有并集的主要原因是与具有并集的C代码进行接口,而C甚至没有析构函数。 对于所有其他目的,似乎最好在Rust代码中使用enum

有一个使用联合实现NoDrop类型的有效用例,该类型禁止删除。

以及通过drop_in_place或类似方式手动调用此类代码。

对我来说,在写入时删除字段值绝对是错误的,因为先前的选项类型未定义。

是否可以禁止现场设置人员,但要求完全更换工会? 在这种情况下,如果并集实现了Drop,则将按预期替换值来调用完全并集drop。

我认为禁止现场设置员是没有道理的。 大多数使用联合的用法都应该没有问题,并且没有Drop实施的字段很可能仍然是常见情况。 带有实现Drop的字段的并集默认情况下会产生警告,从而使意外击中该事件的可能性更低。

为了便于讨论,我打算公开对联合中字段的可变引用_,然后将任意(可能是Drop )类型放入其中。 基本上,我想使用联合编写自定义节省空间的枚举。 例如,

union SlotInner<V> {
    next_empty: usize, /* index of next empty slot */
    value: V,
}

struct Slot<V> {
    inner: SlotInner<V>,
    version: u64 /* even version -> is_empty */
}

@nikomatsakis我想对当前此处未解决的问题提出具体答案。

为避免不必要的复杂语义,分配给并集字段的行为应类似于分配给struct字段,这意味着删除旧内容。 如果您知道这一点,很容易避免,只需分配给整个联合即可。 这还是有点令人惊讶的行为,但是拥有一个完全实现Drop并集字段将产生警告,并且该警告的文本​​可以明确地将此视为警告。

提供修订了RFC1444的RFC拉取请求以记录此行为是否有意义?

@joshtriplett由于@nikomatsakis休假,我会回答:我认为这是提交修订RFC来解决此类问题的好方法。 我们通常会在适当的时候快速跟踪此类RFC PR。

@aturon谢谢。 我已将这些clarifications.to归档到新的RFC PR https://github.com/rust-lang/rfcs/issues/1663 ,以解决此问题。

@aturon,您现在可以检查该未解决的问题。)

我在https://github.com/petrochenkov/rust/tree/union有一些初步的实现

状态:已实施(模块错误),已提交公关(https://github.com/rust-lang/rust/pull/36016)。

@petrochenkov太棒了! 到目前为止看起来不错。

我不太确定如何在移动检查器中使用非Copy字段来处理联合。
假设uunion U { a: A, b: B }的初始化值,现在我们移出以下字段之一:

1) A: !Copy, B: !Copy, move_out_of(u.a)
这很简单, u.b也被置于未初始化状态。
完整性检查: union U { a: T, b: T }行为应与struct S { a: T } +字段别名完全相同。

2) A: Copy, B: !Copy, move_out_of(u.a)
假定u.b仍应初始化,因为move_out_of(u.a)只是memcpy而不会以任何方式更改u.b

2) A: !Copy, B: Copy, move_out_of(u.a)
这是最奇怪的情况。 假定u.b尽管是Copy也应该置于未初始化状态。 Copy值可以未初始化(例如let a: u8; ),但是将它们的状态从初始化更改为未初始化是新的AFAIK。

@ retep998
我知道这完全与FFI的需求无关:)
好消息是它不是阻止程序,我将实施任何更简单的行为并在本周末提交PR。

@petrochenkov我的直觉是,工会本质上是一个“位桶”。 您负责跟踪数据是否已初始化以及其真实类型是什么。 这与原始指针的引用非常相似。

这就是为什么我们不能为您删除数据的原因,也是为什么对字段的任何访问都不安全的原因(即使只有一个变体)。

根据这些规则,如果工会为他们实施了复制,我希望工会能够实施Copy 。 但是,与struct / enum不同,它不会进行内部完整性检查:如果愿意,您始终可以实现联合类型的复制。

让我举一些例子来澄清:

union Foo { ... } // contents don't matter

该联合是仿射的,因为尚未实现Copy

union Bar { x: Rc<String> }
impl Copy for Bar { }
impl Clone for Bar { fn clone(&self) -> Self { *self } }

此联合类型Bar是副本,因为已实现Copy

请注意,如果Bar是一个结构,则由于字段x的类型而实现Copy是错误的。

,我重新阅读了,但我想我实际上并没有回答您的问题。 =)

好的,所以,我知道我根本没有回答您的问题。 所以,让我再试一次。 遵循“位桶”原则,我仍然希望我们可以随意退出工会。 但是当然,另一种选择是将其像对待*mut T一样对待,并要求您使用ptr::read搬出。

编辑:实际上我不确定,为什么我们会禁止这样的举动。 它可能不得不执行w / moving drop动作-也许仅仅是因为容易犯错,并且使“ move out”更加明确似乎更好? 我很难记住这里的历史。

@nikomatsakis

我的直觉是,工会本质上是一个“位桶”。

相反,哈,我想为这样一个危险的结构提供尽可能多的关于工会内容的保证。

解释是,联合是一个我们不知道判别式的枚举,即,我们可以保证在任何时候至少联合的一个变体中的至少一个具有有效值(除非涉及不安全的代码)。

当前实现中的所有借入/移动规则都支持此保证,同时这也是最保守的解释,它使我们可以采用“安全”方式(例如,允许安全访问具有相同类型字段的联合,这可以是有用)或将来的“位桶”方式,这将为Rust工会带来更多的经验。

实际上,我想使其更加保守,如https://github.com/rust-lang/rust/pull/36016#issuecomment -242810887中所述

@petrochenkov

解释是,联合是一个我们不知道判别式的枚举,即,我们可以保证在任何时候至少联合的一个变体中的至少一个具有有效值(除非涉及不安全的代码)。

请注意,使用联合时,总是涉及不安全的代码,因为对字段的每次访问都是不安全的。

我认为我的想法是相似的。 基本上,一个工会就像一个枚举,但可以同时存在多个变体。 有效变体集在任何时候都不是编译器所知道的,尽管有时我们可以确定该变体为空(即,枚举未初始化)。

因此,我认为对some_union.field任何使用基本上都是一个隐式(且不安全的)断言,即有效变体集当前包含field 。 这似乎与借位检查程序集成的工作方式兼容。 如果您借用字段x然后尝试使用y ,则会出现错误,因为您基本上是在说数据同时是xy (它是借来的)。 (相比之下,使用常规枚举是不可能一次居住多个变体,您可以在rowck规则发挥作用的方式中看到这一点)。

无论如何,关键是,当我们从联合的一个字段“移动”时,我想知道的问题是我们是否可以推断出这意味着将值解释为其他变量不再有效。 我认为,无论哪种方式争论都不会那么困难。 我认为这是一个灰色区域。

保守的危险是,我们可能会排除不安全的代码,否则这些代码将变得有意义并且有效。 但是我可以从紧缩开始并决定是否稍后放松。

我们应该讨论在工会上实施Copy需要什么条件的问题-而且,我们应该确保我们具有上面列出的这些灰色区域的完整列表,以确保在稳定之前进行处理和记录!

基本上,一个工会就像一个枚举,但可以同时存在多个变体。

反对“多个变量”解释的一个论据是联合在常量表达式中的行为-对于这些联合,我们总是知道单个活动变量,也不能访问非活动变量,因为在编译时进行转换通常是不好的(除非我们正在尝试将编译器变成某种部分目标仿真器)。
我的解释是,在运行时,非活动变体仍处于非活动状态,但是如果它们与联合的活动变体(更具限制性的定义)或与联合的片段分配历史(更加模糊,但更有用)在布局上兼容,则可以访问它们。

我们应该确保我们具有这些灰色区域的完整列表

我将在不太遥远的将来修改联合RFC! “枚举”的解释具有非常有趣的结果。

在编译时进行转换通常是不好的(除非我们试图将编译器变成某种部分目标仿真器)

@petrochenkov这是我的Miri项目的目标之一。 Miri已经可以进行转换和各种原始指针的恶作剧。 使Miri处理并集将花费很少的工作(在原始内存处理方面没有新内容)。

@eddyb力争用Miri版本替换Rustc常量评估。

@petrochenkov

反对“多个变量”解释的一个论据是联合在常量表达式中的表现方式。

如何最好地支持在常量中使用并集是一个有趣的问题,但我认为将常量表达式限制为运行时行为的子集没有问题(无论如何,这是我们始终要做的事情)。 也就是说,仅因为我们可能无法在编译时完全支持某些特定的转换,并不意味着它在运行时是非法的。

我的解释是,在运行时非活动变量仍处于非活动状态,但如果它们在布局上与联合的活动变量兼容,则可以对其进行访问

嗯,我试图思考这与说工会同时属于所有这些变体有何不同。 我还没有真正的区别。 :)

我觉得这种解释与动作总体上具有奇怪的相互作用。 例如,如果数据“确实”是X,而您将其解释为Y,但Y是仿射的,那么它是否仍然是X?

无论如何,我认为任何领域的举动都会消耗整个联盟是可以的,可以认为与这些解释中的任何一个都一致。 例如,在“变体集”方法中,想法只是移动值会取消所有现有变体的初始化(当然,您使用的变体必须是有效集合之一)。 在您的版本中,它似乎会“转换”为该变体(并消耗原始变体)。

我将在不太遥远的将来修改联合RFC! “枚举”的解释具有非常有趣的结果。

这样的自信! 您将尝试;)

希望就您打算进行哪些具体更改提供更多详细信息?

希望就您打算进行哪些具体更改提供更多详细信息?

实现的更详细说明(即更好的文档),一些小的扩展(如空联合和联合模式中的.. ),联合演化的两个主要(矛盾)替代方法–更不安全且限制更少的“临时空间”解释和更安全,更严格的“具有未知判别的枚举”解释-以及它们对移动/初始化检查器, Copy impls, unsafe ty访问字段等的影响。

定义何时访问无效的联合字段是UB也很有用,例如

union U { a: u8, b: () }
let u = U { b: () };
let a = u.a; // most probably an UB, equivalent to reading from `mem::uninitialized()`

但这是一个棘手的领域。

听起来可能,跨域语义基本上是正确的指针吗?
_(_()为* u8)

2016年9月1日,星期四,Vadim Petrochenkov [email protected]
写道:

定义何时访问无效的联合字段也很有用
是UB,例如

联合U {a:u8,b:()}
让u = U {b:()};
让a = ua; //很可能是UB,相当于从mem::uninitialized()读取

但这是一个棘手的领域。

-
您收到此消息是因为您已订阅此线程。
直接回复此电子邮件,在GitHub上查看
https://github.com/rust-lang/rust/issues/32836#issuecomment -244154751,
或使线程静音
https://github.com/notifications/unsubscribe-auth/ABxXhi68qRITTFW5iJn6omZQQBQgzweNks5qlw4qgaJpZM4IDXsj

现场访问是否总是不安全的?

2016年9月1日,星期四,Vadim Petrochenkov [email protected]
写道:

希望就您打算进行哪些具体更改提供更多详细信息?

实施的更详细说明(即更好
文档),一些小扩展(例如,空联合和.. in union
模式),联合发展的两个主要(矛盾)替代方案-更多
不安全且限制性较小的“临时空间”解释,更安全
和更严格的“具有未知判别的枚举”的解释-和
对移动/初始化检查器的影响,复制提示,不安全
现场访问等

-
您收到此消息是因为您已订阅此线程。
直接回复此电子邮件,在GitHub上查看
https://github.com/rust-lang/rust/issues/32836#issuecomment -244151164,
或使线程静音
https://github.com/notifications/unsubscribe-auth/ABxXhuHStN8AFhR3KYDU27U29MiMpN5Bks5qlws9gaJpZM4IDXsj

现场访问是否总是不安全的?

有时可以使其安全,例如

  • 分配给可微毁的联合字段是安全的。
  • 对“ union U { f1: T, f2: T, ..., fN: T }字段(即所有字段具有相同类型)的任何访问在“具有未知判别的枚举”解释中都是安全的。

从用户的角度看,最好不要对此应用特殊条件。 总是把它称为不安全。

目前正在git的最新rustc中测试对工会的支持。 我尝试过的所有东西都完美运行。

我在死区检查器中遇到了一个有趣的案例。 尝试以下代码:

#![feature(untagged_unions)]

union U {
    i: i32,
    f: f32,
}

fn main() {
    println!("{}", std::mem::size_of::<U>());
    let u = U { f: 1.0 };
    println!("{:#x}", unsafe { u.i });
}

您会收到此错误:

warning: struct field is never used: `f`, #[warn(dead_code)] on by default

好像dead_code检查器没有注意到初始化。

(我已经就“结构字段”的使用提交了PR#36252,将其更改为“字段”。)

联盟当前不能包含动态大小的字段,但是RFC并未通过以下两种方式指定此行为:

#![feature(untagged_unions)]

union Foo<T: ?Sized> {
  value: T,
}

输出:

error[E0277]: the trait bound `T: std::marker::Sized` is not satisfied
 --> <anon>:4:5
  |
4 |     value: T,
  |     ^^^^^^^^ trait `T: std::marker::Sized` not satisfied
  |
  = help: consider adding a `where T: std::marker::Sized` bound
  = note: only the last field of a struct or enum variant may have a dynamically sized type

上下文关键字在模块/板条箱根上下文外部不起作用:

fn main() {
    // all work
    struct Peach {}
    enum Pineapple {}
    trait Mango {}
    impl Mango for () {}
    type Strawberry = ();
    fn woah() {}
    mod even_modules {
        union WithUnions {}
    }
    use std;

    // does not work
    union Banana {}
}

好像是一个非常讨厌的一致性疣。

@nagisa
您是否偶然使用了旧版本的rustc?
我刚刚检查了您在游戏围栏上的示例,它可以工作(模“空联合”错误)。
还有一个针对这种情况的运行通过测试-https: //github.com/rust-lang/rust/blob/master/src/test/run-pass/union/union-backcomp.rs。

@petrochenkov啊,我用过play.rlo,但似乎它可能已还原为stable或其他东西。 那没关系

我认为工会最终将需要支持“安全领域”,这是该提案中不安全领域的邪恶双胞胎。
抄送https://github.com/rust-lang/rfcs/issues/381#issuecomment -246703410

我确实认为,有必要根据各种标准来声明“安全联盟”。

例如,一个仅包含“复制非拖放”字段(均具有相同大小)的联合看起来很安全; 无论您如何访问这些字段,都可能会获得意外的数据,但不会遇到内存安全问题或未定义的行为。

@joshtriplett您还需要确保可以读取未初始化数据的类型中没有“漏洞”。 正如我想说的:“未初始化的数据要么是不可预测的随机数据,要么是您的SSH私钥,以较糟者为准。”

&T不是复制和非删除吗? 将其放入带有usize的“安全”联合中,您将获得一个恶意的参考生成器。 因此,规则必须比这更严格。

例如,一个仅包含“复制非拖放”字段(均具有相同大小)的联合看起来很安全; 无论您如何访问这些字段,都可能会获得意外的数据,但不会遇到内存安全问题或未定义的行为。

我意识到这只是一个现成的示例,不是一个认真的建议,但是这里有一些示例说明了这是多么的棘手:

  • u8bool具有相同的大小,但是大多数u8值对于bool无效,而忽略此会触发UB
  • &T&U具有相同的大小,并且对于所有TU都是Copy + !Drop U (只要两个或两个都不是Sized
  • 当前只能在不安全的代码中在uN / iNfN进行转换。 我相信这样的变形总是安全的,但这确实扩大了安全语言的范围,因此可能会引起争议。
  • 侵犯隐私(例如,在struct Foo(Bar);Bar作弊)是一个很大的缺点,因为隐私可能被用来维护与安全相关的不变量。

@Amanieu在写

@cuviper我试图定义“普通旧数据”,例如包含零指针的东西。 没错,定义需要排除引用。 将一组允许的类型以及这些类型的组合列入白名单可能更容易。

@rkruppe

u8和bool具有相同的大小,但是大多数u8值对于bool无效,并且忽略此值会触发UB

好点子; 同样的问题也适用于枚举。

&T和&U具有相同的大小,并且对于所有T和U都是Copy +!Drop(只要两个或两个都没有调整大小)

我忘记了

目前仅在不安全的代码中才能在uN / iN和fN之间进行转换。 我相信这样的变形总是安全的,但这确实扩大了安全语言的范围,因此可能会引起争议。

双方都同意; 这似乎可以接受。

违反隐私(例如在struct Foo(Bar);和Bar之间进行修整)是一个很大的缺点,因为隐私可能会被用来维护与安全相关的不变量。

如果您不知道该类型的内部,则无法知道内部是否满足要求(例如没有内部填充)。 因此,您可以通过要求所有组件递归地为纯旧数据并且具有足够的可见性来验证这一点,从而排除此情况。

目前仅在不安全的代码中才能在uN / iN和fN之间进行转换。 我相信这样的变形总是安全的,但这确实扩大了安全语言的范围,因此可能会引起争议。

浮点数具有信号NaN,它是陷阱的表示,导致UB。

@ retep998 Rust是否可以在不支持禁用浮点陷阱的任何平台上运行? (这不会改变UB问题,但从理论上讲,我们可以解决该问题。)

@petrochenkov

解释是,联合是一个我们不知道其判别式的枚举,即,我们可以保证在任何时候至少联合的一个变体中的至少一个具有有效值

我认为我已经理解了这种解释-嗯,不是这样。 我仍然认为这是因为,正如我一贯所做的那样,存在一些法律变体,它们是在您存储时确定的。 我认为将值存储到工会中就像将其置于“量子状态”一样-现在可以将其转换为许多法律解释之一。 但是我同意,当您从这些变体之一中移出时,您已经将其“强制”为其中之一,并消耗了价值。 因此,您将无法再次使用该枚举(如果该类型不是Copy )。 所以👍,基本上。

关于#[repr(C)] :正如@pnkfelix最近向我指出的那样,当前规范指出,如果工会不是#[repr(C)] ,则使用字段x进行存储并读取是非法的与字段y 。 大概是因为我们不需要以相同的偏移量开始所有字段。

我可以在其中看到一些实用程序:例如,清理程序可以通过将联合存储为普通枚举(甚至是struct ...?)并检查您使用的变量是否相同来实现联合。

但是,这似乎是一种步枪,也是其中的一项保证,我们将永远无法实际上改变实践,因为太多的人将在野外依靠它。

有什么想法吗?

@nikomatsakis

解释是,联合是一个我们不知道其判别式的枚举,即,我们可以保证在任何时候至少联合的一个变体中的至少一个具有有效值

最糟糕的部分是变体/字段片段,联合可以直接访问它们。
考虑以下代码:

union U {
    a: (u8, bool),
    b: (bool, u8),
}
fn main() {
    unsafe {
        let mut u = U { a: (2, false) };
        u.b.1 = 2; // turns union's memory into (2, 2)
    }
}

所有字段均为Copy ,不涉及所有权,并且移动检查器很高兴,但是对非活动字段b的部分赋值使联合成为具有0有效变体的状态。 我还没有想过如何处理它。 做这样的任务UB? 改变解释? 还有吗

@petrochenkov

做这样的任务UB?

这是我的假设,是的。 当您分配a ,变量b不在有效变量集中,因此以后使用u.b.1 (无论是读取还是分配)是无效的。

关于#[repr(C)]的问题:正如@pnkfelix最近向我指出的那样,当前规范指出,如果联合不是#[repr(C)],则用字段x存储并用字段y读取是非法的。 大概是因为我们不需要以相同的偏移量开始所有字段。

我认为此处的适当措辞是:1)从与先前编写的字段/字段片段“不兼容布局”(这是模糊的)的字段中读取内容是UB 2)对于#[repr(C)]工会用户知道什么布局是(来自ABI文档),以便他们可以区分UB和非UB 3)对于#[repr(Rust)]工会布局未指定,因此用户无法说出什么是UB,什么不是,而是WE(rustc / libstd +他们的)测试)具有这一神圣知识,因此我们可以将小麦与谷壳分开,并以非UB方式使用#[repr(Rust)]

4)确定大小/跨度和字段重新排序问题后,我希望将结构和联合的布局设置为石头并指定,因此用户也将知道这些布局,并且可以使用#[repr(Rust)]联合像#[repr(C)]一样自由,问题将消失。

@nikomatsakis在对联合RFC的讨论中,人们提到希望拥有本机的Rust代码,该代码使用联合来构建紧凑的数据结构。

使用#[repr(C)]人还能阻止什么吗? 如果不是,那么我认为不需要为#[repr(Rust)]提供任何形式的担保,只需将其保留为“ here be dragons”即可。 对于不是#[repr(C)]工会,默认情况下最好对它进行警告是最好的。

@ retep998在我看来repr(Rust)不保证任何特定的布局或重叠是合理的。 我只是建议repr(Rust)在实践中不应破坏人们对联合的内存使用的假设(“不大于最大成员”)。

Rust是否可以在不支持禁用浮点陷阱的任何平台上运行?

这确实不是一个有效的问题。 首先,优化器本身可以依靠陷阱表示的UB-ness并以意外的方式重写程序。 而且,Rust也不真正支持更改FP环境。

但这似乎是一种步枪,也是我们永远无法真正改变实践的一种保证,因为太多的人将在野外依赖它。

有什么想法吗?

如果在可靠地将枚举写入其他字段的情况下从某个字段读取了内容,则添加一个用于检查程序流程的绒毛或类似内容可以帮助用户¹。 基于MIR的皮棉将简化其工作。 如果CFG不允许对联合现场载荷的合法性做出任何结论,并且用户犯了错误,则未定义行为是我们可以指定的最佳行为,而无需指定Rust repr本身IMO。

¹:如果人们出于某种原因开始使用工会作为穷人的trans葬,则特别有效。

在实践中不应破坏人们对联合的内存使用的假设(“不大于最大成员”)。

我不同意。 例如,在某些体系结构上,将repr(Rust)内容扩展

在稳定之前可能要考虑的一个问题是https://github.com/rust-lang/rust/issues/37479。 看起来与最新版本的LLDB调试联合可能不起作用:(

@alexcrichton是否可以与GDB一起使用?

据我所知,是的。 Linux机器人似乎可以很好地运行测试。

这意味着Rust提供了所有正确的调试信息,而LLDB只是这里有一个错误。 我不认为多个调试器之一中的错误(在另一个调试器中不存在)不应妨碍稳定这一点。 LLDB仅需要修复。

很高兴看到我们是否可以在1.17周期(即3月16日测试版)中将该功能加入FCP。 谁能概述未解决的问题和功能的当前状况,以便我们能够确定是否可以达成共识并解决所有问题?

@withoutboats
我的计划是

  • 等待即将发布的版本(2月3日)。
  • 提议使用Copy字段稳定联合。 这将满足FFI的所有需求-FFI库将能够在稳定状态下使用联合。 “ POD”联合在C / C ++中使用了数十年,并且众所周知(基于模类型的别名,但是Rust没有),也没有已知的阻止程序。
  • 在2月3日之前编写“ Union 1.2” RFC ,它将描述并集的当前实现并概述未来的方向。 具有非Copy字段的联合会的未来将在讨论此RFC的过程中确定。

请注意,从标准库中公开ManuallyDropNoDrop之类的东西不需要稳定的并集。

状态更新(2月4日):我正在编写RFC,但是像往常一样,我在每个句子后面都写上了作者的段落,因此我有可能在下个周末(2月11日至12日)而不是本周末完成(2月4-5日)。
状态更新(2月11日):文本已经准备好95%,我明天将提交。

@petrochenkov似乎是非常合理的行动。

@petrochenkov在我

@joshtriplett我当时在想,虽然在@ rust-lang / lang会议上我们讨论了使清单保持最新,但我实际上希望看到-针对以上几点,我们做出了肯定的决定(即,最好使用@rfcbot)。 这可能会提出一个不同的问题(甚至是修订RFC)。 我们可以随着时间的推移执行此操作,但是直到那时我才觉得我们已经确定地“解决”了未解决问题的答案。 沿着这些思路,将相关对话提取并汇总到RFC修订中,或者甚至是我们可以从此处链接到的问题,这似乎是帮助确保每个人都在同一页面上的绝佳步骤-任何感兴趣的人都可以做当然,不只是@ rust-lang / lang成员或牧羊人。

因此,我已经提交了“联盟1.2” RFC- https://github.com/rust-lang/rfcs/pull/1897。

现在,我想建议对联合的保守子集进行稳定化-联合的所有字段都应为Copy ,字段的数量应为非零且联合不应该实现Drop
(不过,我不确定最后一个要求是否可行,因为可以通过将联合包装到结构中并为该结构实现Drop来轻松绕开它。)
这种联合满足了FFI库的所有需求,而FFI库应该是此语言功能的主要使用者。

RFC“ Union 1.2”的文本并没有真正告诉FFI样式联合的任何新内容,只是它明确确认允许使用类型修剪。
编辑:“ Union 1.2” RFC也将使对简单可破坏的Copy字段的分配安全(请参阅https://github.com/rust-lang/rust/issues/32836#issuecomment-281296416,https ://github.com/rust-lang/rust/issues/32836#issuecomment-281748451),这也会影响FFI样式的联合。

该文本还提供了稳定所需的文档。
可以将“概述”部分复制粘贴到书中,将“详细设计”复制粘贴到参考书中。

ping @nikomatsakis

确实需要将类似这样的内容作为语言的一部分添加吗? 我花了大约20分钟的时间才用了一些unsafeptr::write()取消了联合的实现。

use std::mem;
use std::ptr;


/// A union of `f64`, `bool`, and `i32`.
#[derive(Default, Clone, PartialEq, Debug)]
struct Union {
    data: [u8; 8],
}

impl Union {
    pub unsafe fn get<T>(&self) -> &T {
        &*(&self.data as *const _ as *const T)
    }

    pub unsafe fn set<T>(&mut self, value: T) {
        // "transmute" our pointer to self.data into a &mut T so we can 
        // use ptr::write()
        let data_ptr: &mut T = &mut *(&mut self.data as *mut _ as *mut T);
        ptr::write(data_ptr, value);
    }
}


fn main() {
    let mut u = Union::default();
    println!("data: {0:?} ({0:#p})", &u.data);
    {
        let as_i32: &i32 = unsafe { u.get() };
        println!("as i32: {0:?} ({0:#p})", as_i32);
    }

    unsafe {
        u.set::<f64>(3.14);
    }

    println!("As an f64: {:?}", unsafe { u.get::<f64>() });
}

我觉得对于某个人来说,编写一个可以生成类似宏的宏并不困难,除了确保内部数组的大小最大。 然后,可以添加一个特性来限制您可以获取和设置的类型,而不是我完全通用的(并且非常不安全) get::<T>() 。 如果要命名字段,甚至可以添加特定的getter和setter方法。

我在想他们可能会这样写:

union! { Foo(u64, Vec<u8>, String) };

我的意思是,这是您可以切实可行地作为库的一部分进行的操作,而不是向已经相当复杂的语言添加额外的语法和复杂性。 加上proc宏,即使它还没有完全达到稳定状态,这也是很有可能的。

@ Michael-F-Bryan我们还没有常量size_of

@ Michael-F-Bryan仅仅拥有一个[u8]数组还不够,还需要正确地对齐。 实际上,我实际上已经使用宏来处理联合,但是由于缺少常量size_ofalign_of我必须手动分配正确的空间,而且因为声明性宏中没有可用的标识并置,必须手动指定getter和setter的名称。 即使现在初始化工会也很困难,因为我必须先使用一些默认值对其进行初始化,然后将该值设置为所需的变量(或添加另一组方法来构造工会,这在定义中更为冗长联盟)。 与工会的本机支持相比,这总的来说需要做更多的工作,而且容易出错和丑陋。 也许您应该阅读RFC及其附带的讨论,这样您才能理解为什么此功能如此重要。

和对齐相同。

我想现在syn存在,标识级联应该不会太困难。 它允许您对传入的AST进行操作,因此您可以使用两个Idents ,提取其字符串表示形式( Ident实现AsRef<str> ),然后创建一个新的Ident是两个使用Ident::From<String>()的串联。

RFC提到了很多有关现有宏实现的使用方法的麻烦,但是随着最近创建诸如synquote类的板条箱,现在制作proc宏要容易得多。 我觉得这将大大改善人体工程学并减少出错的可能性。

例如,您可能有一个MyUnion::default() ,它只是联合的内部缓冲区为零,然后是fn MyUnion::new<T>(value:T) -> MyUnion ,其中T具有一个特征绑定,确保您只能使用正确的类型进行初始化。

在对齐方式和大小方面,您是否可以使用标准库中的mem模块(即std :: mem :: align_of()和好友)? 我想我提出的所有内容都取决于能否在宏扩展时使用它们来确定所需的大小和对齐方式。 99.9%的联合使用时间都是通过原始类型完成的,因此我觉得您可以编写一个辅助函数,该函数接受类型的名称并返回其对齐方式或大小(可以询问编译器,尽管更多实施细节)。

我承认,内置模式匹配会非常好,但是大多数时候,无论如何,在FFI中使用的任何联合都会被包裹在一个薄的抽象层中。 因此,您可能可以摆脱一些if / else语句或使用辅助函数。

在对齐方式和大小方面,您是否可以使用标准库中的mem模块(即std :: mem :: align_of()和好友)?

在任何交叉编译环境中,这都是行不通的。

@ Michael-F-Bryan所有这些讨论以及更多讨论都在https://github.com/rust-lang/rfcs/pull/1444的历史中进行。 除了已经提到的那些问题之外,总结对您的特定问题的响应:您必须重新实现每个目标平台/编译器的填充和对齐规则,并在整个FFI代码中使用笨拙的语法( @ retep998实际上已经完成了此操作)广泛用于Windows绑定,并且可以证明()的笨拙。 另外,proc宏当前仅可用于派生。 您无法将语法扩展到其他地方。

也:

总而言之,使用联合的99.9%的时间都是通过原始类型完成的

完全不对。 C代码广泛使用“结构联合的结构”模式,其中大多数联合字段由不同的结构类型组成。

@rfcbot fcp合并每个@petrochenkov的评论https://github.com/rust-lang/rust/issues/32836#issuecomment -279256434

我没有什么可添加的,只是触发了机器人

团队成员@withoutboats建议将其合并。 下一步是由其他带标签的团队审核:

  • [x] @aturon
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @withoutboats

当前没有问题。

一旦这些评论者达成共识,这将进入其最终评论期。 如果您发现在此过程中尚未提出的重大问题,请大声说出来!

请参阅本文档,以获取有关带标签的团队成员可以给我哪些命令的信息。

PSA:我将对“ Unions 1.2” RFC进行更新,并进行其他更改,以影响FFI样式的联合-我将安全分配从“未来方向”移到联合的琐碎的联合字段中,改为RFC本身。

union.trivially_destructible_field = 10; // safe

为什么:

  • 不管对联合的解释如何,对可微毁的联合字段的分配都是无条件安全的。
  • 它将删除与工会相关的unsafe块的大约一半。
  • 由于稳定代码中潜在的大量unused_unsafe警告/错误,以后将很难执行。

@petrochenkov您是说“易碎的领域的工会”?

您是否建议所有不安全行为都是在阅读时发生的,您可以在其中选择一种解释? 例如,有一个包含枚举和其他字段的联合,联合值包含无效的判别式?

从高水平看,这似乎是合理的。 它确实允许某些我认为不安全的事情,但Rust通常不允许这样做,例如绕过析构函数或内存泄漏。 在低水平上,我会犹豫地考虑那种声音。

我可以稳定该子集。 我还不知道我对此Unions 1.2 RFC的看法,因为我还没有时间阅读它! 我不确定在某些情况下允许安全访问字段的想法。 回想起来,我觉得我们对不安全行为的“最小化”努力(只是取消引用指针)是一个错误,我们应该宣布更广泛的不安全事物(例如,大量强制转换),因为它们与LLVM进行复杂的交互。 我觉得这里也可能是这种情况。 换句话说,我宁愿撤回有关unsafe的规则,

@joshtriplett
“微不足道的领域”,我调整了措辞。

您是否建议所有不安全行为都是在阅读时发生的,您可以在其中选择一种解释?

是。 没有后续读取,仅凭写入就不会造成任何危险。

编辑:

回想起来,我觉得我们对不安全行为的“最小化”尝试(只是取消引用指针)是一个错误。

哦。
安全写完全符合当前的安全性方法,但是如果您要更改它,那么我应该等待。

我对稳定这个子集并不满意。 通常,当我们稳定一个子集时,它是一个语法子集或至少相当明显的子集。 这个子集对我来说有点复杂。 如果对该功能还有很多未定的决定,我们还没有准备好稳定当前的实现,那么我宁愿让整个过程保持不稳定一段时间。

@nrc
好吧,子集非常明显-“ FFI联合”或“ C联合”或“ C ++ 11之前的联合”-即使它不是语法。 我的最初目标是尽快稳定此子集(理想情况下,此循环),以便可以在winapi类的库中使用它。
剩余的子集及其实现没有什么特别可疑的,它不是紧急的,需要等待不确定的时间,直到“ Union 1.2” RFC的过程完成。 我的期望是在稳定初始子集后的1、2或3个周期内稳定其余部分。

我认为我对安全领域分配有最终的论据。
不安全的现场分配

unsafe {
    u.trivially_destructible_field = value;
}

等效于安全的完整联合分配

u = U { trivially_destructible_field: value };

除了安全的版本是矛盾的安全的,因为它会覆盖u “s字节之外trivially_destructible_field与undefs,而字段赋值有关于让他们完整的保证。

@petrochenkov极端是size_of_val(&value) == 0吧?

仅当相关字段为时,两个摘要之间的对等才为真
“极易破坏”,不是吗?

从这种意义上讲,使这样的作业安全,但仅在某些情况下
对我来说似乎非常矛盾。

2017年2月22日14:50,“ Vadim Petrochenkov”通知@ github.com
写道:

我认为我对安全领域分配有最终的论据。
不安全的现场分配

不安全{
u.trivially_destructible_field =值;
}

等效于安全的完整联合分配

u = U {trivially_destructible_field:value};

除了安全版本反而是安全性较低外,因为它将
使用undefs覆盖trivially_destructible_field之外的u字节,
而字段分配可以保证完整无缺。

-
您收到此邮件是因为有人提到您。
直接回复此电子邮件,在GitHub上查看
https://github.com/rust-lang/rust/issues/32836#issuecomment-281660298
或使线程静音
https://github.com/notifications/unsubscribe-auth/AApc0lUOXLU5xNTfM5PEfEz9nutMZhXUks5rfC8UgaJpZM4IDXsj

@eddyb

极端是size_of_val(&value)== 0,对吧?

是的

@nagisa
我不明白,为什么另外一条消除大量误报的简单规则极其不一致。 “仅某些情况”尤其涵盖所有FFI工会。
我认为“极端不一致”是一个很大的高估。 不像“只能分配mut变量?严重的不一致!”那样大,但是仍然朝着这个方向发展。

@petrochenkov请考虑这种情况:

// Somebody Somewhere in some crate (v 1.0.0)
struct Peach; // trivially destructible
union Banana { pub actually: Peach }

// Somebody Else in their dependent crate
extern some crate;
fn somefn(banana: &mut Banana) {
    banana.actually = Peach;
}

现在,由于添加特质实现通常不是一项重大变化,因此Somesome Somewhere先生认为添加以下实现可能是一个好主意

impl Drop for Peach { fn drop(&mut self) { println!("Moi Peach!") }

并发布板条箱的1.1.0(与1.0.0 AFAIK兼容的版本)版本。

突然,其他人的箱子不再编译了:

fn somefn(banana: &mut Banana) {
    banana.actually = Peach; // ERROR: Something something… unsafe assingment… somewhat somewhat trivially indestructible… 
}

因此,有时允许对联合字段进行安全分配并不像仅mut本地变量可更改那样简单。


老实说,当我写下这个例子时,我已经不确定应该采取哪种立场。 一方面,我想保留以下特性:添加实现通常不是一项重大更改(忽略XID的潜在情况)。 另一方面,将联合的任何领域从平凡的可破坏性更改为非平凡的可破坏性显然是一个与semver不兼容的更改,这是非常容易忽略的,并且提议的规则将使这些不兼容性更加明显(只要分配不处于不安全的范围内)已经)。

@nagisa
这是一个很好的论据,我没有考虑过兼容性。

这个问题似乎可以解决。 为避免兼容性问题,请执行与一致性相关的操作-避免否定性推理。 即用最接近的正近似值替换“ trivially-destructible” ==“没有组件实现Drop ”-“ implements Copy ”。
Copy类型不能向后兼容取消实现Copy ,而Copy类型仍然代表大多数“易破坏”类型,尤其是在FFI联合的情况下。

实现Drop已经不向后兼容,这与并集功能无关:

// Somebody Somewhere in some crate (v 1.0.0)
struct Apple; // trivially destructible
struct Pineapple { pub actually: Apple }

// Somebody Else in their dependent crate
extern some crate;
fn pineapple_to_apple(pineapple: Pineapple) -> Apple {
    pineapple.actually
}
// some crate v 1.1.0
impl Drop for Pineapple { fn drop(&mut self) { println!("Moi Pineapple!") }
fn pineapple_to_apple(pineapple: Pineapple) -> Apple {
    pineapple.actually // ERROR: can't move out of Pineapple
}

这反过来听起来像是实现Drop删除隐式Copy。 并复制
可以依靠。

2017年2月22日星期三上午10:11,jethrogb [email protected]写道:

实现Drop已经不向后兼容,并且具有
与联合功能无关:

//某人放在某个板条箱中(v 1.0.0)
构造苹果; //容易破坏
struct Pineapple {实际上是酒吧:Apple}

//其他人在其依赖的板条箱中
放一些箱子
fn pineapple_to_apple(菠萝:菠萝)->苹果{
菠萝实际上
}

//一些箱子v 1.1.0
impl Drop for Pineapple {fn drop(&mut self){println!(“ Moi Pineapple!”)}

fn pineapple_to_apple(菠萝:菠萝)->苹果{
banana.actually //错误:无法移出菠萝
}

-
您收到此消息是因为您已订阅此线程。
直接回复此电子邮件,在GitHub上查看
https://github.com/rust-lang/rust/issues/32836#issuecomment-281752949
或使线程静音
https://github.com/notifications/unsubscribe-auth/ABxXhgbFgRNzYOsU4c6Gu1KFfwdjDHn3ks5rfHpYgaJpZM4IDXsj

@jethrogb
我想提到这个问题,但是没有,因为它有一些相当特殊的前提条件-实现Drop应该有一个公共字段,而该字段不应是Copy 。 联合案例无条件地影响所有结构。

@petrochenkov我认为也许建立一个工会应该是不安全的论点:)

@petrochenkov用于稳定子集的开发和夜间用户体验路径是什么? 我们是否首先为子集添加了新的特征门,以便人们在子集变得稳定之前可以使用它获得具体的经验?

@pnkfelix
我假设只是为了不再需要此子集#[feature(untagged_unions)] ,没有任何新功能或其他官僚机构。
FFI风格的工会被认为是最常用的工会,因此新功能将意味着在稳定之前就保证有破损,我认为这很烦人。

我只想指出,因为还没有实现对齐和打包属性(没关系稳定),所以我真的没有强烈的需求来稳定它。

@ retep998
填料? 如果您的意思是#[repr(packed)]那么工会现在支持它(不同于align(>1)属性)。

@petrochenkov #[repr(packed(N))] 。 在Winapi中,需要打包1以外的其他文件。 这并不是说我需要工会特别支持这些功能,我只是不想跳到新的主要版本来提高我的最低Rust要求,除非我可以同时获得所有这些功能。

为了在这里阐明游戏状态:

当前的FCP提案仅适用于纯Copy工会。 据我所知,除了“我们应该稳定下来”以外,基本上没有其他悬而未决的问题。 自移交给FCP以来的讨论全部是关于

@nrc@nikomatsakis ,从关于IRC的讨论中,我怀疑你们俩都准备好了检查您的盒子,但我会把它留给您;-)

:bell:根据上面的评论

@petrochenkov
在我看来,这将从我最近提出的想法中受益。 (https://internals.rust-lang.org/t/automatic-marker-trait-for-unconditionally-valid-repr-c-types/5054)

虽然没有考虑工会的建议,但所解释的特征Plain (可能受到自行车脱落的影响)将允许使用仅由Plain类型组成的任何工会而不会造成任何安全隐患。 它将属性编码为内存中的_any_位模式同样有效,因此您可以通过强制将内存清零来解决初始化问题,并确保读取无法调用UB。

在FFI的背景下,在许多情况下,定义为Plain也是这样的代码必须具备的事实上要求,而非Plain类型有用的情况很少且难以设置安全地。

除了命名特征的实际存在之外,将这一功能一分为二可能是明智的。 使用不合格的union强制执行要求并允许使用而不会造成不安全的情况,而unsafe union对内容的要求放宽并且使用户更加头痛。 这将使任何一个稳定下来,而不会妨碍将来增加另一个。

@ le-jzr
这似乎与工会充分正交。
我估计在不久的将来接受Plain作为Rust的可能性不大+使更多的联合字段安全访问或多或少是向后兼容的(并非完全由于棉绒),所以我不会因为延迟而推迟对它。

@petrochenkov我并不是建议延迟工会等待我的提案被关闭,而是要考虑这种可能的限制本身的存在。 将来使更多的联合字段安全访问可能会遇到障碍,因为这会造成语言上的更多不一致。 特别地,使得现场访问的安全性在每个现场之间变化似乎是丑陋的。

因此,我的建议是声明unsafe union ,以便以后可以无条件使用安全版本,而无需添加新关键字。 值得注意的是,对于大多数用例而言,完全安全的使用版本就足够了。

编辑:试图澄清我的意思。

这种可能性可能会影响当前和将来设计的另一个地方是初始化。 对于无条件安全的联合,有必要将为其保留的整个内存范围归零。 即使使用当前版本,也要确保这将减少潜在的UB方案,并使使用工会变得更加容易。

最终意见征询期现已完成。

现在,要合并的FCP已经完成,下一步是什么? 将其稳定在1.19会很好。

如果有人可以在该@rfcbot消息中添加更多细节(这是源代码),那就太好了。 对于不熟悉该过程的人来说,这可能会更容易使他们顺其自然地前进。

使它进入1.19的途径很明确。 有人在追它吗? cc @joshtriplett

是否保证NonZero枚举布局优化可以通过union ? 例如, Option<ManuallyDrop<&u32>>不应None为空指针。 Some(ManuallyDrop::new(uninitialized::<[Vec<Foo>; 10]>())).is_some()不应读取未初始化的内存。

https://crates.io/crates/nodrop (用于https://crates.io/crates/arrayvec)具有处理此问题的技巧。

@SimonSapin
目前在RFC中将
在当前的实施方案中

#![feature(untagged_unions)]

struct S {
    _a: &'static u8
}
union U {
    _a: &'static u8
}

fn main() {
    use std::mem::size_of;
    println!("struct {}", size_of::<S>());
    println!("optional struct {}", size_of::<Option<S>>());
    println!("union {}", size_of::<U>());
    println!("optional union {}", size_of::<Option<U>>());
}

版画

struct 8
optional struct 8
union 8
optional union 16

,即不执行优化。
抄送https://github.com/rust-lang/rust/issues/36394

这不太可能达到1.19。

@brson

这不太可能达到1.19。

稳定PR被合并。

带有未标记工会的工会现在以1.19发行(部分来自https://github.com/rust-lang/rust/pull/42068)-在此问题上还剩下什么还是我们应该关闭?

@jonathandturner
仍然有一个非Copy字段的工会世界!
在澄清/文档RFC(https://github.com/rust-lang/rfcs/pull/1897)上,大部分进度被阻止。

自8月以来,具有非Copy字段的工会是否有任何进展? Unions 1.2 RFC似乎已停滞(我猜测是由于展示期而已?)

允许在联合中使用?Sized类型(如果仅用于单一类型的联合)将易于实现https://github.com/rust-lang/rust/issues/47034


Union ManuallyDrop{
值:T
}

@mikeyhew您只需要真正限制一种类型的大小即可。

我正在看一些使用union的Rust代码,不知道它是否调用未定义的行为。

参考[items :: unions]仅提及:

如果非活动字段的布局与联合所保存的当前值充分兼容,则也可以访问它们(使用相同的语法)。 读取不兼容的字段会导致未定义的行为。

但是我在[items :: unions][type_system :: type_layout]中都找不到“兼容布局”的定义。

查看RFC时,我也找不到“兼容布局”的定义,仅是在RFC 1897:Unions 1.2 (未合并)中应该或不应该工作的例子。

RFC1444:联合似乎只允许将联合转换成其变体,只要它不调用未定义的行为,但在这种情况下(我不是),我无法在RFC中找到任何地方。

_precise_规则是否告诉我使用联合的一段代码是否已将定义的行为记录在某个地方(以及定义的行为是什么)?

@gnzlbg初步近似:您可能无法访问填充,可能无法访问包含无效判别式的枚举,可能无法访问包含非真或假值的布尔值,可能无法访问无效或信令浮点值,以及类似的其他内容。

如果您指向涉及工会的特定代码,我们可以查看一下并告诉您它是否在做未定义的事情。

初步近似:您可能不会访问填充,可能不会访问包含无效判别式的枚举,可能不会访问包含非true或false值的布尔值,可能不会访问无效或信令浮点值,并且其他一些类似的东西。

实际上,最新的共识是读取任意浮点数很好(#46012)。

我还要增加一个要求:源和目标并集变量都是#[repr(C)] ,如果它们是结构,则它们的所有字段(以及递归)也都是

@Amanieu我的立场是正确的,谢谢。

所以我想规则没有写在任何地方?

我正在研究如何在新界面中使用stdsimd 。 除非我们用它稳定一些额外功能,否则将需要使用并集对某些simd类型进行类型修剪,如下所示:

https://github.com/rust-lang-nursery/stdsimd/blob/03cb92ddce074a5170ed5e5c5c20e5fa4e4846c3/coresimd/src/x86/test.rs#L17

AFAIK编写一个联合字段并读取另一个联合字段非常类似于使用transmute_copy ,但有相同的限制。 这些限制仍然有些含糊,并不是特定于工会的。

为此,您链接的函数只能使用transmute::<__m128d, [f64; 2]> 。 尽管联合版本可以说是更好的版本,但是至少删除了当前存在的转换:可能只是A { a }.b[idx]

@rkruppe我已经填写了一个剪贴薄的问题以添加该皮棉: https :

您链接的函数只能使用transmute :: <__ m128d i =“ 8”>

我猜我正在寻找的规则是,何时transmute调用未定义的行为(所以我去寻找那些)。

我认为,如果有关工会的语言参考以转换的形式指定了工会的规则(即使转换规则尚未100%明确),而不是仅仅提及“布局兼容性”并留了下来,那将对我有帮助。在那个。 从“布局兼容性”到“如果transmute不会调用未定义的行为,则这些类型与布局兼容并且可以通过punning类型访问”对我来说并不明显。

需要明确的是,transmute [_copy]并不比联合“更原始”。 实际上,transmute_copy实际上只是指针as强制转换加上ptr::readtransmute还需要mem::uninitialized (已弃用)或MaybeUninitialized (一个并集)或类似的东西,并被实现为内在的,以提高效率,但它也可以归结为-修剪memcpy。 我之所以将之与转换联系起来,主要是因为它比较老而且在历史上过分强调,因此我们目前拥有更多专门针对转换的文章和民俗知识。 真正的基本概念决定了什么是有效的,什么是无效的(以及规范将描述的内容),是如何将值以字节形式存储在内存中,以及哪些字节序列应以UB类型读取为UB。

更正:转换实际上并不需要未初始化的存储(通过内部函数,联合或其他方式)。 除了效率,您可以执行以下操作(未经测试,可能包含令人尴尬的错别字):

fn transmute<T, U>(x: T) -> U {
    assert!(size_of::<T>() == size_of::<U>());
    let mut bytes = [0u8; size_of::<U>()];
    ptr::write(bytes.as_mut_ptr() as *mut T, x);
    mem::forget(x);
    ptr::read(bytes.as_ptr() as *const U)
}

转换的唯一“神奇”部分是,它可以在编译时将类型参数限制为相等大小。

参考和and Unions 1.2 RFC在此问题上故意含糊,因为一般而言,转换规则尚未解决。
目的是“对于repr(C)工会,请参见第三方ABI规范,对于repr(Rust)工会,布局兼容性大多未指定(除非如此)”。

重新审视具有放置字段的并集的放置检查语义是否为时已晚?

最初的问题是,添加ManuallyDrop会使约瑟芬不满意,因为(相当顽皮地)它依赖于永久借入的值,而没有先运行析构函数就没有收回其后备存储。

一个简化的示例位于https://play.rust-lang.org/?gist=607e2dfbd51f4062b9dc93d149815695&version=nightly。 这个想法是,有一个类型Pin<'a, T>的方法pin(&'a self) -> &'a T其安全性依赖于不变的“在调用pin.pin() ,如果支持该引脚的内存曾经被回收过,那么销的析构函数必须已运行”。

这个不变量一直由Rust维护,直到添加#[allow(unions_with_drop_fields)]为止,并由ManuallyDrop https://doc.rust-lang.org/src/core/mem.rs.html#949使用。

如果丢弃检查器认为与丢弃字段的并集具有Drop impl,则将恢复不变式。 这是一个重大变化,但是我怀疑任何代码都依赖于当前的语义。

IRC对话: https= 96386869&page

约瑟芬问题: https :

抄送: @nox @eddyb @pnkfelix

最初的问题是,添加ManuallyDrop会导致Josephine不满意,因为(相当顽皮地)它依赖于永久借入的值,而这些值在未运行析构函数的情况下没有收回其后备存储。

析构函数不能保证运行。 Rust不能保证。 它会尝试,但是例如将std::mem::forget为安全函数。

如果丢弃检查器认为与丢弃字段的并集具有Drop impl,则将恢复不变式。 这是一个重大变化,但是我怀疑任何代码都依赖于当前的语义。

工会是不安全的,主要是因为您不知道工会的哪个领域有效。 联合不能具有自动的Drop impl; 如果您想要这样一个隐含符号,则需要考虑到意味着必须知道带有Drop隐含符号的并集字段是否有效的手动编写。

一个澄清一下:我不认为我们应该永远让与工会Drop默认域,但没有至少一个警告-通过默认皮棉,如果不是一个错误的默认皮棉。 unions_with_drop_fields不应在稳定过程中消失。

编辑:哎呀,不是要打“关闭并发表评论”。

@joshtriplett是的,Rust不保证析构函数将运行,但它发生在1.19之前是为了保持不变,即永久借用的值仅在析构函数运行时才被回收。 在mem::forget的情况下,甚至是这样,因为您不能以永久借入的值来调用它。

这是Joephine顽皮地依靠的东西,但由于丢弃检查器如何处理unions_with_drop_fields ,因此不再是真的。

如果allow(unions_with_drop_fields)被认为是不安全的注释,那将是很好的,AFAICT不会有太大的改变,它只需要deny(unsafe_code)来检查allow(unions_with_drop_fields)

@asajeffrey我仍在尝试理解Pin事情...因此,如果我正确地遵循示例,则此“有效”的原因是fn pin(&'a Pin<'a, T>) -> &'a T强制借款持续为只要在类型中对生存期'a注释,并且该生存期是不变的。

这是一个有趣的观察! 我不知道这个技巧。 我的直觉是,这是“偶然”的,即安全的Rust不会提供防止析构函数运行的方法,但这并没有成为“合同”的一部分。 值得注意的是, https: //doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html并未列出泄漏。

IMO不管是偶然还是有意工作。 没有任何方法可以避免Drop在现有ManuallyDrop之前运行此技巧(这需要实现不安全的代码),现在我们不能再依赖它了。

ManuallyDrop基本上杀死了Rust的简洁行为,并说一开始不应该依赖它,这听起来像是对我的循环推理。 如果ManuallyDrop不允许调用Pin::pin ,是否还有其他方法可以使调用Pin::pin听起来不完美? 我不这么认为。

我认为我们不能承诺保留rustc偶然提供的所有保证。 我们不知道这些保证会是什么,因此我们会稳定一头猪(好吧,我希望这个成语是合理的……这就是字典告诉我的与我的母语成语匹配的意思,从字面上可以翻译成“袋子里的猫”;))-我想说的是,我们不知道该稳定什么。

而且,这是一把两刃剑-我们决定提供的每一个额外保证都是不安全的代码必须要处理的事情。 因此,发现新的保证可能会破坏现有的不安全代码(在不知道的情况下静静地坐在crates.io上的某处)与启用新的不安全代码(后者就是这种情况)一样。

例如,非常有可能的是,词法生存期会启用被非词法生存期破坏的不安全代码。 当前所有生命周期都处于良好的嵌套状态,也许有一种不安全的代码可以利用此方法吗? 只有在非词汇生命周期中,生命周期才可以重叠,但是两者都不包含在内。 这是否会使NLL成为一项重大突破? 我希望不是!

如果ManuallyDrop不允许调用Pin :: pin,那么还有其他方法可以使Pin :: pin听起来不好吗? 我不这么认为。

有了unsafe代码,就可以了。 因此,通过此声明Pin招声音,您声明一些不安全的代码unsound如果我们决定声音ManuallyDrop是好的。

我们所描述的是一种非常符合人体工程学的方式,将Rust与GC集成在一起。 我要说的是告诉我这只是偶然的事情,这对我来说听起来是错误的,当我找不到任何不用Drop约束联合的用例时,我们应该忘记它@asajeffrey所描述,这确实是打破约瑟芬的唯一疣。

如果有人能证明即使没有ManuallyDrop ,声音也不是很完美,我会很乐意忘记它。

我要说的是,告诉我们这只是偶然的情况,对我来说听起来很不对劲

我看不出有“设计”过这种技巧的迹象,因此我认为将其称为偶然事件是相当公平的。

而且我们应该忘记它

我应该更清楚地知道这部分只是我个人的直觉。 我认为将其声明为“快乐事故”并实际上将其作为保证也可能是一个合理的行动要点-如果我们有理由相信确实所有其他unsafe代码都遵守此保证,并且提供此保证比ManuallyDrop用例更重要。 这是一个折衷方案,类似于leeppocalypse,在这里我们也不能吃蛋糕,也不能吃蛋糕(我们不能同时使用当前API的Rcdrop基于作用域的范围内的线程;我们不能同时拥有ManuallyDropPin ),所以我们必须以任何一种方式做出决定。

就是说,我发现很难以一种精确的方式来表达这里提供的实际保证,这使我个人更倾向于“ ManuallyDrop很好”的一面。

如果我们有理由相信确实所有其他unsafe代码都遵守此保证,并且提供此保证比ManuallyDrop用例更重要。 这是一个折衷方案,类似于leeppocalypse,在这里我们也不能吃蛋糕,也不能吃蛋糕(我们不能同时使用当前API的Rcdrop基于作用域的范围内的线程;我们不能同时拥有ManuallyDropPin ),所以我们必须以任何一种方式做出决定。

公平地说,我衷心地同意这一点。 请注意,如果我们最终考虑@asajeffrey描述为未定义的行为,则可以带回基于drop的作用域线程API。

据我了解,Alan的建议不是删除ManuallyDrop ,而只是让dropck假定它(以及其他具有Drop字段的并集)具有析构函数。 (那个析构函数碰巧什么也不做,但是它的存在会影响dropck程序接受或拒绝的程序。)

如果有人可以显示它即使没有ManuallyDrop也很不完善,我会很乐意忘记它。

不知道这是否符合条件,但这是我的第一次尝试:在union Rust之前的ManuallyDrop类的愚蠢实现。

pub mod manually_drop {
    use std::mem;
    use std::ptr;
    use std::marker::PhantomData;

    pub struct ManuallyDrop<T> {
        data: [u8; 32],
        phantom: PhantomData<T>,
    }

    impl<T> ManuallyDrop<T> {
        pub fn new(x: T) -> ManuallyDrop<T> {
            assert!(mem::size_of::<T>() <= 32);
            let mut data = [0u8; 32];
            unsafe {
                ptr::copy(&x as *const _ as *const u8, &mut data[0] as *mut _, mem::size_of::<T>());
            }
            mem::forget(x);
            ManuallyDrop { data, phantom: PhantomData }
        }

        pub fn deref(&self) -> &T {
            unsafe {
                &*(&self.data as *const _ as *const T)
            }
        }
    }
}

(是的,我可能需要做更多的工作才能正确对齐,但是也可以通过牺牲一些字节来完成。)
显示此中断的游乐场Pinhttps :

这就是我上面两把剑的意思-据我所知,我的ManuallyDrop尊重我们制定的所有规则。 因此,我们有两个不兼容的不安全代码- ManuallyDropPin 。 谁是对的? 我会说Pin依赖于我们从未做出过的保证,因此在这里是“错误的”,但这只是一个判断,而不是证明。

现在很有趣。 在某些版本的固定资料中, Pin::pin需要&'this mut Pin<'this, T> ,但是让您的ManuallyDrop拥有DerefMut并不是不合理?

这是一个操场,显示@RalfJung的(毫无疑问)仍然使用&mut -采用pin方法来打破Pin

https://play.rust-lang.org/?gist=5057570b54952e245fa463f8d7719663&version=nightly

您的ManuallyDrop具有DerefMut隐含功能并非没有道理,对吗?

是的,我只是添加了此示例所需的API。 明显的deref_mut应该可以正常工作。

据我了解,Alan的建议是不删除ManuallyDrop,而只是让dropck假定它(以及其他具有Drop字段的并集)具有析构函数。 (那个析构函数碰巧什么也不做,但是它的存在会影响dropck程序接受或拒绝的程序。)

啊,我错过了; 对于那个很抱歉。 在我的示例中添加以下内容可使其保持正常工作:

    unsafe impl<#[may_dangle] T> Drop for ManuallyDrop<T> {
        fn drop(&mut self) {}
    }

仅当我删除#[may_dangle] Rust才会拒绝它。 因此,至少,我们必须提出一些违反上述代码的规则-仅说“存在一些我们想听起来与它不兼容的代码”是一个不好的称呼,因为它使看一些代码并检查它是否健全几乎是不可能的。


我认为让我对这种“意外保证”最不满意的是,我没有看到一个行之有效的单一理由。 在Rust中进行连接的方式使这种结合在一起,但是添加dropck并不是为了防止泄漏,而是避免对无效数据的不正确引用(析构函数中的常见问题)。 Pin起作用的理由不是基于“ Rust编译器中有某种​​机制,或者某种类型系统保证,很明显地说永久借用的数据不能泄漏”,而是基于“我们已经尽力而为,无法泄漏永久性借来的数据,因此我们认为还可以。” 依靠它的健全性使我非常紧张。 编辑:涉及dropck的事实使我更加紧张,因为编译器的这一部分具有令人讨厌的健全性错误的历史。 起作用的原因似乎是永久借贷与安全drop不一致。 这似乎确实是“基于对永久性借入的数据可以做什么的详尽案例分析而得出的理由”。

现在,公平地说,关于内部可变性可以说类似的话-碰巧的情况是,如果我们选择正确的API,则允许在某些情况下通过共享引用进行修改实际上是安全的。 然而,使这项工作实际上需要在编译器直接支持( UnsafeCell ),因为它与优化相冲突,并且不安全的代码,这将是不健全内部可变性,但不健全,与室内的可变性。 另一个区别是,内部可变性从一开始就是设计目标(或者从很早开始-这是我在Rust社区之前的方式),而不是“永久借用不会泄漏”。 最后,对于内部可变性,我认为有一个很好的故事:“共享会使变异变得危险,但并非不可能,共享引用的API仅表示您一般不会获得可变性但也不排除允许针对特定的对象进行更多操作类型”,从而产生连贯的整体图景。 当然,我花了很多时间考虑共享引用,所以也许对于我所不知道的问题,存在着同样一致的印象。

时区很有趣,我只是起床而已! 这里似乎有两个问题(通常是不变式,尤其是dropck),因此我将它们放在单独的注释中...

@RalfJung :是的,这是不安全的Rust维护不变量的问题。 对于任何版本的Rust + std,使用依赖保证推理维护不变的I不止一种选择。 确实可能有两个库L1L2 ,它们选择了不兼容的I1I2 ,因此Rust + L1是安全的,并且Rust + L2是安全的,但是Rust + L1 + L2是不安全的。

在这种情况下, L1ManuallyDropL2Josephine ,很明显ManuallyDrop将会赢了,因为现在在std ,它比Josephine具有更强的向后兼容性约束。

有趣的是, https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html上的准则写为:“编写不安全代码时,程序员的责任是不可能让安全代码表现出以下行为:...”也就是说,它是一个上下文属性(对于所有安全上下文C,C [P]都不会出错),因此取决于版本(因为Rust + std v1.20具有更安全的特性)上下文高于v1.18)。 特别是,我声称钉扎确实在1.20之前满足了Rust的这一约束,因为没有安全的上下文C st C [钉扎]出错。

但是,这只是营房里的律师,我想每个人都同意这种上下文定义存在问题,因此所有关于不安全代码准则的讨论都在进行。

如果没有别的,我认为固定显示了意外不变式出错的有趣例子。

未加标签的并集(以及因此ManuallyDrop )所做的特殊事情是在与drop checker的交互中,尤其是ManualDrop行为类似于其defn:

unsafe impl<#[may_dangle] T> Drop for ManuallyDrop<T> { ... }

然后就可以进行是否允许的对话了:)确实,这种对话在https://github.com/rust-lang/rust/issues/开始的may_dangle线程中发生了34761#issuecomment -362375924

@RalfJung您的代码显示了一个有趣的data的运行时类型为T ,但其编译时类型为[u8; N] 。 就may_dangle而言,哪种类型至关重要?

有趣的是, https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html上的准则写为:“编写不安全代码时,程序员的责任是不可能让安全代码表现出以下行为:...”即是上下文属性

啊,有趣。 我同意这显然是不够的-这会使原始范围内的线程听起来不错。 为了有意义,这必须(至少)指定允许调用安全代码的一组不安全代码。

就我个人而言,我觉得指定此内容的更好方法是给出要维护的不变式。 但是我在这里显然有偏见,因为我用来证明有关Rust的方法需要这样的不变性。 ;)

我对页面没有某种形式的初步声明感到惊讶。 正如讨论所显示的,我们还不确定确切的限制是多少。 我们要求使用不安全的代码至少可以执行该文档中所述的内容,但是我们可能需要执行更多操作。

例如,不确定的行为,什么不安全的代码可以做到的极限是一样的。 有关该主题的最新讨论,请参见https://github.com/nikomatsakis/rust-memory-model/issues/44 :为mem::size_of::<T>() == 0复制&mut T不会导致任何未定义的行为直接执行,但显然对于不安全的代码而言是非法的。 原因是其他不安全的代码可能依赖于其所有权准则的遵守,而重复的事情违反了该准则。

如果没有别的,我认为固定显示了意外不变式出错的有趣例子。

哦,那当然。 我想知道将来如何避免这种情况? 也许对https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html发出了一些大警告,说“仅仅是因为不变式恰好保存在rustc + libstd中,并不意味着不安全的代码可以依靠它;这里是您可以依靠的一些不变式?”?

@RalfJung是的,我认为没有人会喜欢“正确性”的上下文定义,主要是因为它对上下文的观察力很脆弱。 就不变式而言,我会对语义定义感到满意。

我唯一要问的是,请给我们一些摆动空间,并为依赖保证推理定义两个不变量(代码可以依赖R并应保证G,其中G表示R)。 这样,就有一些空间可以增强R和减弱G。如果我们只有一个不变性(即R = G),我们将永远无法改变它们!

常量检查当前不属于特殊情况的联合字段:(cc @solson @ oli-obk)

union Transmute<T, U> { from: T, to: U }

const SILLY: () = unsafe {
    (Transmute::<usize, Box<String>> { from: 1 }.to, ()).1
};

fn main() {
    SILLY
}

上面的代码产生miri评估错误“调用非常量fn std::ptr::drop_in_place::<(std::boxed::Box<std::string::String>, ())> - shim(Some((std::boxed::Box<std::string::String>, ()))) ”。

更改它以强制const-checker观察.to的类型:

const fn id<T>(x: T) -> T { x }

const SILLY: () = unsafe {
    (id(Transmute::<usize, Box<String>> { from: 1 }.to), ()).1
};

导致“无法在编译时评估析构函数”。

相关的实现代码在这里(特别是restrict调用):
https://github.com/rust-lang/rust/blob/5e4603f99066eaf2c1cf19ac3afbac9057b1e177/src/librustc_mir/transform/qualify_consts.rs#L557

对#41073的更好分析表明,分配给并集的子字段时析构函数运行时的语义尚不足以进行稳定化。 有关详情,请参见该问题。

完全排除联合中的Drop类型并单独实现ManuallyDrop (作为lang-item)是否现实? 据我所知, ManuallyDrop似乎是Drop工会的最大动机,但这是一个非常特殊的情况。

如果没有正负值,则可以说如果每个字段都是CopyManuallyDrop<T>形式的联合,则格式正确。 这将完全避免分配联合字段时丢弃的所有复杂性(似乎所有可能的解决方案都充满了令人惊讶的步枪),而ManuallyDrop对程序员来说是一个清楚的标记,他们必须处理Drop自己在这里。 (检查可能更聪明,例如,它可以遍历在同一包装箱中声明的产品类型和名义类型。当然,以肯定的方式说“此类型将永远不会实现Drop ”更好)。


第一篇文章中的清单没有提到未合并大小的联合,也没有提到RFC-,但是我们仍然

这与有时在C中使用工会(这是一个“扩展点”)的方式相冲突(IIRC @joshtriplett曾是所有提到这一点的人):头文件可能会为工会声明3个变体,但这被认为是向前兼容的,以后再添加更多变体(只要不增加并集的大小即可)。 如果标签(坐在其他地方)表明他们不知道当前变体,则库的用户保证不触摸联合数据。 重要的是,如果你只知道一个单一的变异体,这并不意味着有只有一个单一的变异体!

支票可能更聪明,例如可以遍历产品类型和同一包装箱中声明的名义类型。

该谓词已经存在,但由于没有绑定特征,因此在泛型中比较保守。
您可以通过std::mem::needs_drop (使用rustc实现的内在函数)访问它。

@eddyb会考虑needs_drop向前兼容性,还是会乐于研究其他创建项以确定它们的类型是否实现Drop ? 这里的目标是要进行一次检查,该检查永远不会脱离与semver兼容的更改,例如,在没有类型或生存期参数且仅私有字段的结构中添加impl Drop即可。

@RalfJung

这与有时在C中使用工会(这是一个“扩展点”)的方式相冲突(IIRC @joshtriplett曾是所有提到这一点的人):头文件可能会为工会声明3个变体,但这被认为是向前兼容的,以后再添加更多变体(只要不增加并集的大小即可)。 如果标签(坐在其他地方)表明他们不知道当前变体,则库的用户保证不触摸联合数据。 至关重要的是,如果您只知道一个变体,那并不意味着只有一个变体!

这是一个非常具体的情况。
它仅影响从C标头生成的C样式的并集(因此没有析构函数,并且所有内容都是Copy ,正是稳定的可用子集)。
我们可以轻松地向此类联合添加_dummy: ()_future: ()字段,并且默认情况下继续受益于更安全的“枚举”模型。 FFI工会是“扩展点”,无论如何,都需要有据可查。

在太平洋标准时间2018年4月17日10:08:54,Vadim Petrochenkov [email protected]写道:

我们可以轻松地向此类联合添加_dummy: ()_future: ()字段
并默认继续受益于我们更安全的“枚举”模型。

我已经看到人们谈论将工会像枚举一样对待,但我们并不知道这些判别式,但是据我所知,我不知道它们的任何实际模型或处理方式。 在最初的讨论中,甚至非FFI工会都希望使用“一次有效的多个变体”模型,其中包括根本不需要FFI工会的激励用例。

添加()变种工会应该不会改变任何东西,工会应该被要求这样做来获得他们所期望的语义。 联合应该继续是一小袋,Rust不知道在任何给定时间它们可能包含什么,直到不安全的代码访问它为止。

FFI工会
作为“扩展点”是需要有据可查的
无论如何。

当然,我们应该尽可能准确地记录语义。

@RalfJung不,它的行为就像auto trait s一样,公开了所有内部细节。

当前围绕“活动字段”进行了一些讨论,并通过以下网址加入了联合:

联合应该继续是一小袋,Rust不知道在任何给定时间它们可能包含什么,直到不安全的代码访问它为止。

这正是我期望工会发挥作用的方式。 它们是一项高级功能,可挤压出额外的性能并与C代码进行交互,而C语言中没有诸如析构函数之类的东西。

对我来说,如果要删除并集的内容,则必须将其强制转换/转换(也许不能转换,因为它可能更大,最后还有一些未使用的位用于另一个变体)您要drop〜获取需要删除的字段的指针,并使用std::ptr::drop_in_place ,或使用字段语法提取值。

如果我对工会一无所知,这就是我希望他们运作的方式:

示例-将mem::uninitialized为联合

pub union MaybeValid<T> {
    valid: T,
    invalid: ()
}

impl<T> MaybeValid<T> {
    #[inline] // this should optimize to a no-op
    pub fn from_valid(valid: T) -> MaybeValid<T> {
        MaybeValid { valid }
    }

    pub fn invalid() -> MaybeValid<T> {
        MaybeValid { invalid: () }
    }

   pub fn zeroed() -> MaybeValid<T> {
        // do whatever is necessary here...
        unimplemented!()
    }
}

fn example() {
    let valid_data = MaybeValid::from_valid(1_u8);
    // Destructor of a union always does nothing, but that's OK since our 
    // data type owns nothing.
    drop(valid_data);
    let invalid_data = MaybeValid::invalid();
    // Destructor of a union again does nothing, which means it needs to know 
    // nothing about its surroundings, and can't accidentally try to free unused memory.
    drop(invalid_data);
    let valid_data = MaybeValid::from_valid(String::from("test string"));
    // Now if we dropped `valid_data` we would leak memory, since the string 
    // would never get freed. This is already possible in safe rust using e.g. `Rc`. 
    // `union` is a similarly advanced feature to `Rc` and so new users are 
    // protected by the order in which concepts are introduced to them. This is 
    // still "safe" even though it leaks because it cannot trigger UB.
    //drop(valid_data)
    // Since we know that our union is of a particular form, we can safely 
    // move the value out, in order to run the destructor. I would expect this 
    // to fail if the drop method had run, even though the drop method does 
    // nothing, because that's the way stuff works in rust - once it's dropped
    // you can't use it.
    let _string_to_drop = unsafe { valid_data.valid };
    // No memory leak and all unsafety is encapsulated.
}

我将发布此内容,然后对其进行编辑,这样我就不会失去工作。
编辑@SimonSapin删除字段的方式。

如果要删除并集的内容,则必须将其强制转换/转换(也许无法转换,因为它可能更大,最后还有一些未使用的位用于另一个变体),将其转换为要删除的类型,或使用字段语法提取值

(如果只是放下它,就不需要移动它,就可以提取该值,您可以将指针指向一个字段并使用std::ptr::drop_in_place 。)

相关:对于常量,我目前正在争论常量中联合的至少一个字段需要正确: https :

我将发布此内容,然后对其进行编辑,这样我就不会失去工作。

请注意,修改不会反映在电子邮件通知中。 如果您要对评论进行重大更改,请考虑替代或附加做出新评论。

@derekdreery (以及其他所有人),我会对您对https://internals.rust-lang.org/t/pre-rfc-unions-drop-types-and-manuallydrop/8025的反馈意见感兴趣

相关:对于常量,我目前正在争论常量中联合的至少一个字段需要正确:#51361

我已经看到了实现,但没有看到参数。 ;)

嗯...“根本不检查似乎很奇怪”的论点。

我很乐意实施const checker中提出的任何方案,但我的直觉始终是,工会的一个变体需要完全正确。

否则,联合只是指定具有特定大小和对齐方式的类型的一种好方法,并且某些编译器为在固定类型集之间进行转换提供了便利。

我认为联合会是“未解释的比特袋”,具有一些方便的访问方式。 我对不检查它们一点都不奇怪。

AFAIK实际上在柏林全能赛中提到了一些用例

我认为联合会是“未解释的比特袋”,具有一些方便的访问方式。 我对不检查它们一点都不奇怪。

尽管这种解释总是与语言的精神背道而驰。
在其他地方,我们使用静态分析来防止踩踏,请检查未访问未初始化或借入的值,但对于突然禁用分析的联合,请射击。

我认为这正是union 。 我的意思是我们也有原始指针,所有分析都被禁用。 联合可以完全控制数据布局,就像原始指针可以完全控制内存访问一样。 两者都以安全为代价。

而且,这使union简单。 我认为简单很重要,当涉及到不安全的代码时,尤为重要(工会总是如此)。 如果这里提供了切实的好处,我们应该只接受额外的复杂性。

我们认为不必为工会付出这笔费用,因为“位袋”模型与带有未知变量的枚举模型相比并没有提供任何新的机会。

这里所讨论的属性至少是维护不安全代码的负担,同时也是一种保护措施。 没有静态分析可以防止所有可能破坏此属性的错误,因为我们确实想对不安全的类型punning 1使用联合,因此“带有未知变量的枚举”实际上意味着代码处理联合必须非常谨慎地对待如何写入联合union或具有风险的即时UB,而不会真正减少从union中读取所涉及的不安全性,因为读取已经需要(通过编译器无法理解的通道)知道这些位对于您正在读取的变体有效。 实际上,我们只能警告用户有关在miri下运行时对其任何变体均无效的联合,而在绝大多数情况下,它不会在运行时发生。

1例如,为简单起见,假定元组为re​​pr(C),则union Foo { a: (bool, u8), b: (u8, bool) }允许您仅通过字段分配构造无效的内容。

@rkruppe

Union Foo {a:(bool,u8),b:(u8,bool)}

嘿,那是我的例子:)
并且在RFC 1897模型下有效(“叶”片段bool -1, u8 -1, u8 -2, bool至少一个) -2在任何部分分配后有效)。

代码处理联合必须非常小心如何写入联合或冒着即时UB的风险

这就是RFC 1897模型的重点,静态检查可确保没有安全的操作(例如赋值或部分赋值)可以将联合变为无效状态,因此您不必一直非常小心,也不必立即获得UB 。
只有与联合无关的不安全操作(例如通过通配指针进行写操作)才能使联合无效。

另一方面,没有移动检查,工会可以很容易地进入无效状态。

let u: Union;
let x = u.field; // UB

这就是RFC 1897模型的重点,静态检查可确保没有安全的操作(例如赋值或部分赋值)可以将联合变为无效状态,因此您不必一直非常小心,也不必立即获得UB 。
只有与联合无关的不安全操作(例如通过通配指针进行写操作)才能使联合无效。

您可以自动识别某些类型的写入,而不违反施加在并集上的额外不变式,但是仍然需要作者坚持额外的不变式。 由于读取仍然是不安全的,并且需要手动确保这些位对所读取的变体有效,因此这实际上对读取者没有帮助,只会使写入者的生活更加艰难。 “位袋”或“具有未知变体的枚举”都不能解决联合的难题:如何确保其实际存储要读取的数据类型。

出色的类型检查将如何影响Dropping? 如果创建一个联合,然后将其传递给拥有所有权的C,那么rust会尝试释放数据,可能导致双重释放吗? 还是您总是自己实现Drop

编辑那将是非常酷,如果工会就像“里的变体在编译时静态检查枚举”,如果我理解的建议

编辑2工会可以从一袋零钱开始,然后在向后兼容的同时允许安全访问吗?

并且在RFC 1897模型下有效(“叶子”片段bool-1,u8-1,u8-2和bool-2中的至少一个在任何部分分配后均有效)。

如果我们决定我们希望这是有效的,我认为@ oli-obk应该更新miri的支票以反映这一点-将https://github.com/rust-lang/rust/pull/51361合并后,它将被拒绝米里

@petrochenkov已经处在不安全的环境中,因此事情自然会更加复杂。 我认为我们应该有明确的动机来说明为什么这种额外的复杂性值得。 我认为“它在某种程度上违反了语言的精神”并不是明确的动机。

我能想到的一件事是布局优化。 在“零碎的袋子”模型中,工会从来没有任何利基。 但是,我认为通过为程序员提供对利基市场的更多手动控制来更好地解决这一问题,这在其他情况下也很有用

我想我这里缺少基本的东西。 我同意@rkruppe的观点
工会的难题是确保工会当前存储
程序要读取的数据。

但是AFAIK不能通过静态分析“局部”解决此问题。 我们
至少需要整个程序分析,即使那样,它仍然是
一个很难解决的问题。

那么...桌上有没有解决这个问题的方法? 或者,
提出的确切解决方案实际上可以购买我们吗? 说我从C得到一个联合,
如果不分析整个Rust和C程序,建议可以
静态分析实际上可以为读者提供保证吗?

@gnzlbg我认为我们唯一得到的保证就是@petrochenkov在上面写的内容

静态检查可确保没有安全的操作(如赋值或部分赋值)会使联合变为无效状态

另一方面,没有移动检查,工会可以很容易地进入无效状态。

您的建议也无法防止误读,我认为这是不可能的。

此外,我还设想了一些非常基本的“初始化”跟踪,即“写入任何字段会初始化联合”。 允许impl Drop for MyUnion时,我们还是需要一些东西。 无论是好是坏,我们都必须决定何时以及在何处插入自动的挂断呼叫。 这些规则应该尽可能地简单,因为这是我们要插入现有的细微不安全代码中的额外代码。 对于确实实现Drop ,我还想象到一个类似于struct ,除非已经初始化了数据结构,否则该限制不允许写入字段。

@derekchiang

工会可以从一小袋开始,然后在允许向后兼容的同时允许安全访问吗?
不。一旦我们说这只是一小袋,假定允许的话,可能会有不安全的代码。

我认为最基本的移动检查中是否有价值,以查看是否初始化了联合。 原始RFC明确指定,初始化或分配给任何联合字段都将初始化整个联合。 但是,除此之外,rustc不应尝试推断用户未明确指定的并集中的值。 联合可能根本不包含任何值,包括对其任何字段无效的值。

例如,一个用例:考虑一个C样式的带标签的联合,该联合在将来可以显式地扩展带有更多标签。 读取联合的C和Rust代码不能假定它知道每种可能的字段类型。

@RalfJung

也许我应该从另一个方向开始。

此代码是否应1)适用于工会2)适用于非工会?

let x: T;
let y = x.field;

对我而言,在两种情况下答案都是显而易见的“否”,因为这是Rust可以并且希望防止的一整类错误,无论T的“联合”性质如何。

这意味着移动检查器应具有某种实现其支持的方案。 考虑到移动检查器(和借用检查器)通常以逐字段方式工作,联合的最简单方案是“与结构的相同规则+(取消)字段的初始化/借用,也(取消)初始化/借用其同级字段”。
这个简单的规则涵盖了所有的静态检查。

然后,枚举模型只是上述静态检查加上一个或多个条件的结果。
如果1)启用了初始化检查,并且2)不安全的代码未将任意无效字节写入属于联合的区域,则联合“叶子”字段之一将自动有效。 这是动态不可检查的(至少对于具有> 1个字段的联合并且在const-evaluator之外)保证,但它首先针对的是阅读代码的人员。

例如,来自@joshtriplett的这种情况

例如,一个用例:考虑一个C样式的带标签的联合,该联合在将来可以显式地扩展带有更多标签。 读取联合的C和Rust代码不能假定它知道每种可能的字段类型。

如果工会明确为“可能的将来扩展”增加了一个字段,那么对于阅读代码的人来说,这将更加清晰。

当然,我们可以保留基本的静态初始化检查,但可以拒绝第二个条件,并允许通过某些不安全的“第三方”方式将任意可能无效的数据写入联合,而无需将其作为即时UB。 那样我们就不再拥有以人为本的动态担保了,我只是认为那是净亏损。

@petrochenkov

此代码是否应1)适用于工会2)适用于非工会?

let x: T;
let y = x.field;

对我而言,在两种情况下答案都是显而易见的“否”,因为这是Rust可以并且希望防止的一整类错误,无论T的“联合”性质如何。

同意,此级别的未初始化值检查似乎是合理的,并且非常可行。

这意味着移动检查器应具有某种实现其支持的方案。 考虑到移动检查器(和借用检查器)通常以逐字段方式工作,联合的最简单方案是“与结构的相同规则+(取消)字段的初始化/借用,也(取消)初始化/借用其同级字段”。
这个简单的规则涵盖了所有的静态检查。

假设我了解结构的规则,到目前为止已达成协议。

然后,枚举模型只是上述静态检查加上一个或多个条件的结果。
如果1)启用了初始化检查,并且2)不安全的代码未将任意无效字节写入属于联合的区域,则联合“叶子”字段之一将自动有效。 这是动态不可检查的(至少对于具有> 1个字段的联合并且在const-evaluator之外)保证,但它首先针对的是阅读代码的人员。

该附加条件对工会无效。

例如,来自@joshtriplett的这种情况

例如,一个用例:考虑一个C样式的带标签的联合,该联合在将来可以显式地扩展带有更多标签。 读取联合的C和Rust代码不能假定它知道每种可能的字段类型。

如果工会明确为“可能的将来扩展”增加了一个字段,那么对于阅读代码的人来说,这将更加清晰。

这不是C联合的工作方式,也不是指定Rust联合的工作方式。 (而且我想知道它是否更清楚,还是只是它是否符合一组不同的期望。)更改此设置将使Rust联合会不再适合其设计和提议的某些目的。

当然,我们可以保留基本的静态初始化检查,但可以拒绝第二个条件,并允许通过某些不安全的“第三方”方式将任意可能无效的数据写入联合,而无需将其作为即时UB。 那样我们就不再拥有以人为本的动态担保了,我只是认为那是净亏损。

那些“不安全的“第三方”手段”包括“从FFI获得工会”,这是一个完全有效的用例。

这是一个具体的例子:

union Event {
    event_id: u32,
    event1: Event1,
    event2: Event2,
    event3: Event3,
}

struct Event1 {
    event_id: u32, // always EVENT1
    // ... more fields ...
}
// ... more event structs ...

match u.event_id {
    EVENT1 => { /* ... */ }
    EVENT2 => { /* ... */ }
    EVENT3 => { /* ... */ }
    _ => { /* unknown event */ }
}

这是人们可以并且将使用联合编写的完全有效的代码。

@petrochenkov

此代码是否应1)适用于工会2)适用于非工会?
对我而言,在两种情况下答案都是显而易见的“否”,因为这是Rust可以并且希望防止的一整类错误,而与T的“联合”性质无关。

对我来说没问题。

联合的最简单方案是“与结构相同的规则+(取消)字段的初始化/借用,也(取消)初始化/借用其同级字段”。

哇。 结构规则之所以有意义,是因为它们都是基于不同字段不相交的事实。 您不能仅仅使这个基本假设无效而仍然使用相同的规则。 您需要规则的附录这一事实表明了这一点。 我从不希望像结构一样检查工会。 如果有的话,可能希望像枚举一样对它们进行检查-但当然不能用,因为只能通过match访问枚举。

如果1)启用了初始化检查,并且2)不安全的代码未将任意无效字节写入属于联合的区域,则联合“叶子”字段之一将自动有效。 这是动态不可检查的(至少对于具有> 1个字段的联合并且在const-evaluator之外)保证,但它首先针对的是阅读代码的人员。

我认为对基本有效性假设进行动态检查(给定类型信息)是非常可取的。 然后我们可以在miri的CTFE期间检查它们,甚至可以在“完整”的miri运行期间(例如测试套件)检查它们,我们最终可以使用某种消毒剂或Rust发出debug_assert!在关键位置检查不变量的有效性。
我认为,使用C的不可检查规则的经验提供了充分的证据,证明这些都是有问题的。 通常,真正理解和阐明规则是什么的第一步是找到一种动态可检查的方式来表达它们。 即使对于并发内存模型,“动态可检查”变体(用操作语义解释虚拟机逐步执行的所有内容)也正在出现,并且似乎是解决长期存在的公理化问题的唯一方法先前使用的模型(“稀薄空气问题”是此处的关键字)。

我几乎不能高估拥有动态可检查规则的重要性。 我认为我们的目标应该是拥有0个无法检查的UB案例。 (我们还没有达到目标,但这是我们应该达到的目标。)这是在您的语言中拥有UB的唯一负责任的方式,其他所有情况都是编译器/语言作者的事,这使他们的生活变得更轻松,却牺牲了每个必须承受后果。 (我目前正在为别名和原始指针访问制定动态可检查的规则。)
即使那是唯一的问题,就我而言,“无法动态检查”也有充分的理由不使用这种方法。

就是说,我没有根本原因无法对此进行检查:对于并集中的每个字节,遍历所有变体以查看在此变体中该字节允许使用哪些值,然后采用所有的并集(heh;))这些集合中。 如果根据此定义每个字节均有效,则字节序列对于联合有效。
但是,这实际上很难实现检查-到目前为止,这是我们在Rust中拥有的最复杂的基本类型有效性不变量。 这直接是因为该有效性规则难以描述,这就是我不喜欢它的原因。

当然,我们可以保留基本的静态初始化检查,但可以拒绝第二个条件,并允许通过某些不安全的“第三方”方式将任意可能无效的数据写入联合,而无需将其作为即时UB。 那样我们就不再拥有以人为本的动态担保了,我只是认为那是净亏损。

那保证给我们买什么? 它实际上在哪里有帮助? 现在,我所看到的是每个人都必须努力工作,并谨慎地坚持下去。 我看不到我们人民从中受益。

@joshtriplett

考虑一个C样式的带标签的联合,该联合在将来可以显式地扩展带有更多标签。 读取联合的C和Rust代码不能假定它知道每种可能的字段类型。

@petrochenkov提出的模型通过向联合中添加__non_exhaustive: ()字段来允许这些用例。 但是,我认为这不是必需的。 可以想象,绑定生成器可以添加这样的字段。

@RalfJung

这是动态不可检查的(至少对于具有> 1个字段的联合且在const-evaluator外部)保证

我认为对基本有效性假设进行动态检查是非常可取的

需要说明的是:我的意思是在“默认情况下” /“在发布模式下”是不可检查的,当然,可以通过一些额外的工具在“慢速模式”下进行检查,但是您已经比我写得更好。

@RalfJung

@petrochenkov提出的模型通过向联合中添加__non_exhaustive:()字段来允许这些用例。

是的,我知道那是提议。

但是,我认为这不是必需的。 可以想象,绑定生成器可以添加这样的字段。

他们可以,但是必须将其系统地添加到每个联合中。

我还没有看到一个论点,即为什么有理由打破联合的主要用例,而取而代之的是某些未指定的用例,该用例取决于限制它们可以包含的位模式。

@joshtriplett

工会的主要用例

对于我来说,这根本不是一个主要的用例。
如果您假定repr(C)工会的所有情况都是正确的,那么您假设FFI中所有使用工会用于标记的工会/“ Rust枚举模拟”都假定具有可扩展性(这是不正确的),但是从我的观察中, repr(Rust)联合(投下控制,初始化控制,变形)不会期望“意外变体”突然出现在其中。

@petrochenkov我并没有说“分手主要用途的情况下”,我说“分手主要用例”。 FFI是工会的主要用例之一

并采用所有这些集合的并集(heh;))

“联合的可能值是其所有可能的变体的可能值的并集”这样的说法无疑具有明显的吸引力。

真正。 但是,这不是提议-我们都同意以下内容应合法:

union F {
  x: (u8, bool),
  y: (bool, u8),
}
fn foo() -> F {
  let mut f = F { x: (5, false) };
  unsafe { f.y.1 = 17; }
  f
}

实际上,我认为这甚至是需要unsafe

因此,联合至少必须按字节进行。
而且,我认为“吸引人的明显性”本身并不是足够好的理由。 对于不安全的代码编写者,我们确定的任何不变式都是沉重的负担,我们应该具有反过来得到的具体优势。

@RalfJung

实际上,我认为这甚至是不安全的错误。

我不了解新的基于MIR的不安全检查器的实现,但是在旧的基于HIR的实现中,它肯定是检查器的限制/简化-仅分析expr1.field = expr2形式的表达式以查找可能的“字段”退出“不安全”选项,其他所有内容都被保守地视为对工会不安全的通用“现场访问”。

https://github.com/rust-lang/rust/issues/52786#issuecomment -408645420中回答评论:

因此,想法是编译器仍然对Wrap<T>的合同一无所知,并且无法进行布局优化。 好的,这个位置可以理解。
这意味着在内部,例如,在Wrap模块的内部, Wrap<T>模块的实现可以将“意外值”临时写入其中,如果它不会泄漏给用户,编译器就可以了。

我不确定Wrap合同中有关缺少意外值的部分到底与字段隐私有何关系。

首先,无论字段是私有字段还是公共字段,无法通过这些字段直接写入意外值。 您需要像原始指针之类的东西,或在FFI另一端的代码来执行此操作,并且可以通过仅指向整个联合的指针而无需任何字段访问即可完成。 因此,除了访问受限制的字段之外,我们还需要从其他方向解决这个问题。

正如我对您的评论所解释的那样,方法是说私有字段(无论是联合还是结构都无关紧要)暗含着用户未知的任意不变性,因此更改该字段的任何操作(直接或通过野生指针)都不会。问题)之所以产生UB,是因为它们有可能破坏未指定的不变式。

这意味着,如果一个联合具有单个私有字段,则其实现者(而不是编译器)可以假定没有第三方会在该联合中写入意外值。
从某种意义上说,这是用户的“默认联合文档条款”:
-(默认值)如果联合具有私有字段,则无法在其中写入垃圾内容。
-否则,除非其文档明确禁止,否则您可以将垃圾写入联合。

如果某个工会希望禁止意外值,同时仍提供对预期字段的pub访问(例如,当这些字段没有自己的不变式时),那么它仍然可以通过文档来做到这一点,这就是为什么“除非”第二个子句是必需的。

@RalfJung
这是否可以准确地描述您的位置?

如何处理这种情况?

mod m {
    union MyPrivateUnion { /* private fields */ }
    extern {
        fn my_private_ffi_function() -> MyPrivateUnion; // Can return garbage (?)
    }
}

正如我对您的评论所解释的那样,方法是说私有字段(无论是联合还是结构都无关紧要)暗含着用户未知的任意不变性,因此更改该字段的任何操作(直接或通过野生指针)都不会。问题)之所以产生UB,是因为它们有可能破坏未指定的不变式。

不,那不是我的意思。

有多个不变式。 我不知道我们需要多少,但至少要有两个(而且我没有很好的名字):

  • 类型的“布局级别不变式”(或“语法不变式”)完全由该类型的句法形状定义。 这些是“ &mut T是非NULL且已对齐”,“ bool01 ”,“ !不能存在”。 在此级别上, *mut Tusize -两者都允许任何值(或任何初始化值,但是该区别需要另外讨论)。 最终,我们将有一个文档,通过结构递归来说明所有类型的这些不变量:结构的布局级不变性是其所有字段都保持不变,等等。可见性在这里不起作用。
Violating the layout-level invariant is instantaneous UB. This is a statement we can make because we have defined this invariant in very simple terms, and we make it part of the definition of the language itself. We can then exploit this UB (and we already do), e.g. to perform enum layout optimizations.
  • 类型的“自定义类型级别不变式”(或“语义不变式”)由实现该类型的人选择。 编译器无法知道这个不变式,因为我们没有语言来表达它,语言定义也是如此。 我们不能违反这个不变的UB,因为我们甚至不能说出那个不变的是什么! 甚至可能具有自定义不变量的事实是任何有用的类型系统的一个特征:抽象。 我在过去的博客文章中写了更多有关此

    定制,语义不变量和UB之间的联系是,我们声明不安全代码可能依赖于其外来代码保留的语义不变量。 这就使得继续将任何随机的东西放到Vec的size字段中是不正确的。 请注意,我说的不正确(有时会使用术语unsound )-但不是未定义的行为! 演示这种差异的另一个示例(确实是同一示例)是有关&mut ZST别名规则&mut ZST绝不是立即的UB,但是它仍然是不正确的/不正确的,因为可能会编写不安全的代码,而这依赖于这种情况不会发生。

对齐这两个概念会很好,但是我认为这不切实际。 首先,对于某些类型(函数指针,dyn特性),定制,语义不变式的定义实际上使用语言中的UB定义。 如果我们想说违反自定义,语义不变式是UB,则此定义将是循环的。 其次,我希望我们的语言的定义以及某个执行跟踪是否显示UB是可确定的属性。 语义,自定义不变量通常无法确定。


我不确定Wraps合同中关于缺少意外值的部分与字段隐私有何关系。

本质上,当类型选择其自定义不变量时,它必须确保安全代码可以做的任何事情都可以保留不变量。 毕竟,承诺是,仅使用这种类型的安全API永远不会导致UB。 这适用于结构和联合。 安全代码可以做的事情之一就是访问公共字段,这是连接的来源。

例如,结构的公共字段不能具有与字段类型自定义不变量:毕竟,任何安全用户都可以将任意数据写入该字段,或从该字段读取并期望“良好”数据。 可以安全地构造所有字段都是公共的结构,从而对该字段施加进一步的限制。

一个拥有公共领域的工会……嗯,这有点有趣。 无论如何,读取联合字段都是不安全的,因此在此没有任何更改。 编写联合字段是安全的,因此与公共字段的联合必须能够处理能够满足该字段类型的自定义不变量被放入该字段的任意数据。 我怀疑这会很有用...

因此,回顾一下,当您选择一个自定义不变式时,您有责任确保外国安全代码不会破坏该不变式(并且您拥有专用字段之类的工具来帮助您实现这一目标)。 当该代码执行安全代码无法完成的工作时,外来无用代码有责任不侵犯您的不变式。


这意味着在Wrap的模块内部,实现Wrap例如,模块可以向其中临时写入“意外值”(如果它不会泄漏给用户),则编译器可以使用它们。

正确。 (在这里,紧急安全是一个问题,但您可能知道)。 就像在Vec ,我可以放心地做

let sz = self.size;
self.size = 1337;
self.size = sz;

而且没有UB。


mod m {
    union MyPrivateUnion { /* private fields */ }
    extern {
        fn my_private_ffi_function() -> MyPrivateUnion; // Can return garbage (?)
    }
}

就语法布局不变性而言, my_private_ffi_function可以做任何事情(假设函数调用ABI和签名匹配)。 就语义自定义不变量而言,这在代码中是不可见的-编写此模块的人一旦想到了不变量,就应在其联合定义旁边进行文档记录,然后确保FFI函数返回一个满足不变量的值。

我最后写了一篇博客文章,内容涉及是否必须初始化&mut T ,何时必须初始化,以及上面提到的两种不变式。

是否还有需要跟踪的https://github.com/rust-lang/rust/issues/55149尚未涵盖的内容,还是我们应该关闭?

E0658仍指向此处:

错误[E0658]:具有非Copy字段的并集不稳定(请参阅问题#32836)

由于原子不能实现Copy ,因此当前这与原子非常相关。 有谁知道解决方法?

实施https://github.com/rust-lang/rust/issues/55149后,您将可以在联合中使用ManuallyDrop<AtomicFoo> 。 在此之前,唯一的解决方法是使用Nightly(或不使用union并找到其他方法)。

有了这个实现,您甚至不需要ManuallyDrop ; 毕竟rustc知道Atomic*没有实现Drop

指派自己将跟踪问题切换到新问题。

此页面是否有帮助?
0 / 5 - 0 等级