Skip to content

Latest commit

 

History

History
189 lines (139 loc) · 6.03 KB

0000-pod.md

File metadata and controls

189 lines (139 loc) · 6.03 KB
  • Feature Name: pod
  • Start Date: 2015-03-04
  • RFC PR: (leave this empty)
  • Rust Issue: (leave this empty)

Summary

Split the Copy trait in two traits: Pod and Copy. Pod indicates that a type can be cloned by just copying its bits ("memcpy"), and the new Copy indicates that the type should be implicitly copied instead of moved.

Motivation

Range is not Copy (or deriving Copy is hard)

We have a guideline that says that iterators should not be implicitly copyable (i.e. they shouldn't implement Copy). The Range struct, which is what a..b desugars to, is an iterator, and under this guideline it shouldn't be Copy, but this hurts ergonomics in other cases where it's not used as an iterator, for example a struct that contains Range can't be marked as Copy, because Range is not Copy.

This is a more general problem, if upstream decides that some type shouldn't be Copy, even if it could, then downstream can't make Copyable structs/enums that contain said type.

Cell only admits Copy data

This is unnecessarily restrictive because the data doesn't need to be implicitly copyable, the actual requirement is that the data should be cloneable by just memcpying it.

How splitting Copy helps with this?

Implementing Pod for a type is much less controversial than implementing Copy on it. This should result in more types marked as Pod in libraries, which in turn gives downstream users more control about what can be Copy as the only requirement is that the fields must be Pod.

Cell can be modified to have a Pod bound instead of a Copy one, this would allow more types, like iterators, to be stored in a Cell.

Detailed design

The traits

A Pod marker trait will be added to the stdlib, and the Copy trait will be updated as follows:

/// Indicates that this type can be cloned by just copying its bits (`memcpy`)
#[lang = "pod"]
trait Pod: MarkerTrait {}

/// Indicates that this type will be implicitly copied instead of moved
#[lang = "copy"]
trait Copy: Pod {}

Compiler rules

Update the compiler to enforce the following rules:

  • Pod can only be implemented by structs/enums whose fields are also Pod.
  • Built-in/primitive types that were automatically considered Copy by the compiler (i8, &T, (A, B), [T; N], etc), will now be considered to be both Pod and Copy.
  • Copy restrictions will carry over to Pod, e.g. types with destructors can't implement Pod, nor Copy because of the trait Copy: Pod requirement.

#[derive] syntax extensions

To avoid breaking the world, the #[derive(Copy)] syntax extension will be updated to derive both Copy and Pod:

#[derive(Copy)]
struct Foo<T>(T);

// expands into
impl<T: Pod> Pod for Foo<T> {}
impl<T: Pod> Copy for Foo<T> {}
//^ note that the bound is `T: Pod` (not `T: Copy`!)

Also add a #[derive(Pod)] extension to derive only Pod.

Cell

Cell will be change its T: Copy bound to T: Pod.

Drawbacks

Cognitive load. Currently you have to think whether a type should be Clone, Copy+Clone or neither (affine type). Now you have to choose between Clone, Pod+Clone, Pod+Clone+Copy or neither. (There are other combinations like e.g Pod+Copy, but you almost always want to add Clone in there as well)

Alternatives

Don't do this, figure out how some other way to solve the Range problem (should it be Copy?). And, don't let non-implictly copyable structs (like most iterators) be stored in Cells.

More magic #[derive]s

To reduce the burden of having to write #[derive(Pod, Clone)] and #[derive(Copy, Clone)], the #[derive] syntax extensions could be modified as follows:

  • #[derive(Clone)] derives Clone
  • #[derive(Pod)] derives Pod and Clone
  • #[derive(Copy)] derives Pod, Clone and Copy

This means that for any type you only have to pick one of these #[derive]s or neither.

The author would prefer to obtain the same behavior not with syntax extensions but with a blanket implementation:

impl Clone for T where T: Pod {
    fn clone(&self) -> T {
        unsafe {
            mem::transmute_copy(self)
        }
    }
}

However, this is not actionable without negative bounds because of coherence issues (overlapping implementations).

OIBIT Pod

Another option is to define the Pod trait as an OIBIT. Pod would then have a default implementation, and negative implementations for mutable references and for types with destructors

unsafe trait Pod: MarkerTrait {}
unsafe impl Pod for .. {}
impl<T: Drop> !Pod for T {}
impl<'a, T: ?Sized> !Pod for &'a mut T {}

The resulting behavior matches the built-in (compiler) rules of the current Copy trait.

As in the original proposal, the new Copy trait would become a supertrait over Pod:

trait Copy: Pod {}

This approach has the advantage that is no longer necessary to manually implement Pod as the trait will be implicitly derived whenever the type can be Pod. And, when a type can't be Pod, because e.g. one of its fields is not Pod, then it won't implicitly implement Pod, however this can be overridden with a manual unsafe impl Pod for Ty.

The cases where one must explicitly opt-out of Pod are rare, and involve affine types without destructors. An example is iOS' OsRng:

// not implictly copyable, doesn't implement `Drop`
#[cfg(target_os = "ios")]
struct OsRng {
    _dummy: (),
}

// with this alternative, a negative implementation is required to maintain the
// current semantics
impl !Pod for OsRng {}

The downside of this approach is that by changing the fields of a struct the Podness of the type can be inadvertently (without compiler warning/error) changed as well.

Unresolved questions

  • Is there a name that better describes what Pod is?
  • Should we bring back the "you could implement Pod for type" lint? The author thinks that you almost always want to mark as many things as possible as Pod - that's not the case with the current Copy (e.g. iterators).
  • Should the Pod trait be in the prelude?