Basic Swift ownership-y questions

I have some vague idea about the answers to these questions. Which actually means I have no clue about the answers.

  1. Why exactly can't structs and enums have deinit yet?
    • Do we need move-only struct and enum types, with precise destruction semantics?
  2. Why exactly can't structs and enums have copy constructors yet?
    • Copy constructors seem useful. (why? Swift has survived without them)
    • Swift's semantic model (and implementation in SIL) makes liberal use of copying (copy_value in SIL) for ease-of-understanding and correctness. Copy constructors can fit into that model, right?

Kudos for holistic, design-rationale-providing answers!

@dabrahams

2 Likes

I believe the answer to both of these questions is that the compiler currently assumes copying a struct and then immediately de-initing the copy is side-effect free (all side effects of the copy are precisely undone by the deinit).
This would be untrue if a struct logged in its copy constructor or deinit.

3 Likes

A holistic bag of internally contradictory design rationales:

  • Rule-of-3/5 programming is :japanese_ogre: :japanese_goblin:

    • We managed to avoid it by not having copy constructors/assignment operators
    • C++ interop is probably going to mean that we get it eventually, in some form :frowning_face:
    • I very much hope the vast majority of Swift programmers never have to deal with it
  • Swift today has a very nice property that storing a copy of any type is O(1)

    • Languages with copy constructors with O(N) copy operations, which lead to
    • …people trying to pass things by unsafe pointer to avoid copying cost, which means
    • …bypassing Swift's strict value semantics.
    • But maybe we can discourage the creation and use of such types and because of the CoW precedent, the effect of having copy constructors might not be so bad.
  • If it weren't for C++ interop:

    • I'd disallow structs to have copy constructors or assignment operators
    • I'd disallow copyable structs to have deinit
    • In this world, only move-only and “pinned” structs would have deinit.
    • The language would remain simple
    • Copies would always be O(1)
    • I can't think of many types useful to a Swift programmer that would be ruled out in such a world.
    • Implementing your own reference-counted owner is the one exception
    • Even that seems very much like a niche application that doesn't warrant complicating the language for.

All that said, since I'm working on C++ interop and ownership, I'm having a hard time avoiding the idea that we'll get something very much like full rule-of-5 member support for structs in Swift, eventually. It just needs to be designed and implemented. I hope to design it in such a way that it intrudes on the language as little as possible.

One thing I'm currently mulling over is the idea of importing all nontrivial C++ types as move-only structs in Swift, and providing only explicit __cxx_copy and __cxx_assign operations (spelling to be determined). It's not as crazy as you might think, since pass-by-value can use guaranteed calling convention. This might allow us to stay out of the rule-of-5 business.

5 Likes

This is the only reason I want structs to have copy constructors and destructors (I assume you mean reference counting owner - as in, it implements its own refcount outside of ARC). We don’t have allocators, so this would be the next best thing.

@dabrahams in the context of C++ interop, and just thinking about copy constructors (not move constructors or assignment operators yet), is there a reason that Swift couldn't handle this all behind the scenes? Couldn't we just emit a call to the C++ copy constructor when lowering a copy instruction?

  1. deinit on a copyable type is essentially useless without the ability to change how copying is done. Swift doesn’t have non-copyable types.

  2. Adding custom copying to the language without hamstringing the optimizer is a hard problem. deinit on class types already causes a lot of problems.

  3. I feel pretty strongly that precise lifetime is the wrong tool for pretty much any job; it is better to have a withFoo function, or better yet something analogous with direct support in the language.

3 Likes

will Swift have move-only value types?

Thanks folks!

@Chris_Lattner3, do you have any thoughts here?

I believe you said copy constructors are good/important for Swift, and gave some example involving n-dimension array types ("tensors") and their efficient usage. Maybe you have some insight here.

I see. The answer is Swift needs non-copyable types, pitched as moveonly contexts.

@dabrahams @gribozavr: are you working on move-only types in Swift?

I wonder what's so hard about deinit? (asking because I'm not familiar)

Swift doesn't have precise destruction semantics yet, right? That sounds like it'd make optimizer correctness quite hard - since there is no precise specification of semantics, "correct" isn't defined.

Edit: sounds like potential side effects in deinit are the key issue preventing optimization.

Trailing closure APIs (also known as Python context managers) are pretty great.

// Statically typed languages: trailing closure APIs.
let lines = file.withLines { lines in lines }
# Dynamically typed languages: context manangers.
with open(filename,'r') as file:
  lines = file.readlines()

Dan, I think that you are confusing various levels of semantics.

| dan-zheng
November 29 |

  • | - |

I have some vague idea about the answers to these questions. Which actually means I have no clue about the answers.

  1. Why exactly can't structs and enums have deinit yet?

This would require something like a non-copyable type. That will come with time and isn’t an easy one off effort.

    • Do we need precise destruction semantics (e.g. RAII) for move-only struct and enum types?

Now we don’t want this since it hamstrings the optimizer.

  1. Why exactly can't structs and enums have copy constructors yet?
    • Copy constructors seem useful (why?).

We don’t want to have custom copying in Swift. This is a feature, not a bug.

    • Swift's semantic model (and implementation in SIL) makes liberal use of copying (copy_value in SIL) for ease-of-understanding and correctness. Copy constructors can fit into that model, right?

This is a detail of SIL and a copy_value at the SIL level doesn’t imply anything about Swift level copy constructors.

1 Like

Dan's not wrong that copy_value at the SIL level reflects a semantic copy which, if not optimized, would turn into a use of a source-level "copy constructor". They're not unrelated.

But as you say, the problem with having source-level copy constructors is that (1) they could have arbitrary side-effects but (2) to maintain performance we would need to come up with a language model that still allows them to be elided or reordered, which we've struggled to do for the closely-related problems of destroy_value.

4 Likes

Yep; thought you knew. I'm working on C++ interop and filling out the ownership story is a big part of that.

Depends what level of interop you want to achieve. My goals/priorities are, roughly:

  • API Accessibility : all C++ APIs should be usable from Swift, and all Swift APIs usable from C++, without manual annotation or wrapping.
  • The following take precedence over safety and ergonomics:
    • API Accessibility.
    • Avoiding any performance penalties at the API boundary.
  • The ability to easily make an imported C++ API safe and ergonomic in Swift with manual intervention (annotation, wrapping, etc.) is a goal.
  • Using a C++ API from Swift should not introduce any new, Swift-specific “gotchas,” and vice-versa.

The goal of full API accessibility means you need to be able to subclass a C++ struct in Swift and vice-versa. At some level that means being able to define and use copy constructors. Of course I hope to cleverly hide the complexity somehow, but it has to be there, if hidden.

1 Like

Well, it means being able to use copy constructors. Why do we need to be able to define them in Swift for this? If we look at an example:

//C++
struct X { X(const X &); };
// Swift
struct Y : X { var i : Int }
// IR
swift.Y = type { X, Int }

The subclass essentially turns into a member and we can just decompose and emit the respective copy logic for each class, in this case, we'll call out to X's copy constructor and simply copy i as any other POD type.

The user of X doesn't need to do anything "special" we have the ability to handle this entirely in the compiler (I think). Here's a proof of concept if you're interested.

The compiler needs to be able to arbitrarily remove and insert copy_value as part of standard SSA optimization of SILValues. Yes, it's a semantic copy, but we've defined copyable types such that those copies don't affect program semantics (to the same extent that isUniquelyReferenced isn't allowed to affect program semantics).

The separate issue, already pointed out, is that allowing every copy_value to have arbitrary side effects would be disastrous for optimization in general.

Right, if we did support user-defined copy operations, it would have to be understood that the compiler would be permitted to optimize types with custom copy operations just like it would optimize any other value-copy. I think the basic problem is that it's hard to define precisely what that would mean — it must mean that certain kinds of side effects would not be allowed within them, but which effects, exactly?

We do need to think about this. C++ interoperation is a long-term goal for the language, and that includes importing non-trivial types. Normal C++ copy constructors and destructors generally have side effects that are "outside the model" that the Swift optimizer works within — they're not going to e.g. write to Swift-accessible memory. Unfortunately, that isn't guaranteed: you can certainly write a C++ type whose copy-constructor calls a function that you stored into the source object. Presumably we need to make that illegal in some way to preserve our optimization goals. So we're forced to think about this because of C++ even if we never have native custom copying.

I wonder if we can reasonably just enumerate a list of side-effects that are legal in custom copy-constructors and destructors. The most important thing to allow is allocation and deallocation, as well as accesses to memory that's only accessible through the object being constructed/destroyed. It's not unreasonable to say that any other memory that it accesses has to be marked somehow to make it "volatile". Would we also want copy operations to guarantee not to destroy any values?

Ideally we could even force class deinits to obey these same restrictions. That bird may have flown, though.

2 Likes

If I want to create a type in Swift to be used by C++ code, it seems to me I may need to be able to control how C++ copies it.

I think the idea floated at the bottom of this post may solve this problem for copy constructors. What are the specific issues with deinit?

Do you have a particular use case in mind where a Swift type would need a custom copy constructor so that C++ could copy the type properly?

My thinking was that we might not need this because no one has ever been able to have Swift types with custom copy constructors, and, as I think Micheal said, this is a feature. So, unlike C++, we wouldn't be "taking away" anything from people.

The optimizer folks can speak to that better than I can, but I believe the issue is that, since class deinit can have arbitrary side-effects, it is very tricky to do certain kinds of optimizations that reorder things around releases without analyzing the possible side-effects of a release.