This helped clarify where I think the disconnect is.
Yes, I think this is it. Thank you for calling this out :)
My expectation is that when I'm writing code that uses C++ APIs from Swift I should be thinking like I'm writing C++, similar to how when I'm writing code that uses C APIs from Swift I'm thinking like I'm writing C. That means eliminating the early-lifetime gotchas that Swift currently has with RAII types, but not many other changes.
Ignoring whether this is something that would be beneficial to do, I am not sure it's possible.
On the other hand, your goal seems to be that you should be able to treat using C++ APIs from Swift like writing any other Swift code. To an extent, I think that's a worthy goal. I guess I'm just worried that that won't be achievable at scale (that as soon as you're doing something unusual you're back to writing C++ in Swift) and that it may introduce performance issues (for things like game engines sometimes you really do want -Ounchecked in release, and you avoid Swift reference counting wherever possible). How successful what you're trying to do will end up being will depend on how often users fall off the happy path and end up having to write unsafe code; I see that as being pretty often, but maybe (hopefully) I'm wrong.
I think this is a good summation of our disagreement. I think we both understand the other's argument, and just disagree on whether this will be implementable, how often users will fall off the "happy path," and the balance of annotations vs automatic imports.
For the second point, I would be genuinely interested to see some pseudo-code/snippets of Swift where you want to use an example API that doesn't fit into one of these patterns. If nothing else, that will help us find what patterns to tackle next.
I think that's a little disingenuous â for the third example, a wrapper function around begin() is no less safe than calling begin() directly, and "works" so long as the vector is kept alive/not mutated.
That is not correct. I think this is highlighting my point very well, actually (especially considering that this is the only example we couldn't fix with all the ownership features and lifetime rules under the sun). The result of the call to findStart is always a dangling pointer. There is no valid way to use the result of findStart.
I don't think it's super relevant to the discussion, but I will explain why each of these produces a dangling reference, because it's kind of interesting if nothing else. In reverse order:
func findStart(ofVector v: vector) -> Iter { v.begin() }
As I mentioned before, this will also produce a dangling reference in C++. What's happening here is the vector is being copied from the caller into v. The copy allocates some new storage. At the end of the function, v is destroyed (and the storage is deallocated), but the iterator/pointer is still the same (referencing the deallocated storage) so the caller gets back a dangling reference even if the argument is still alive. This could be fixed with v: inout vector (as that would remove the copy).
let v = vector(1)
let start = v.begin()
doSomething(start)
This one is pretty strait forward: Swift has different lifetime rules than C++ so v might be destroyed before start's last use. This could be fixed if we somehow associated the two lifetimes. (Or maybe with lexical lifetimes?)
var v = vector(1)
let start = v.begin()
doSomething(start)
fixLifetime(v)
Finally, this one is the most complicated. Here the issue is that we are doing an "lvalue to rvalue" conversion or a "mutable to immutable" conversion. What this means is we need to convert the mutable var v into something immutable, which we do with a copy. You might be asking, "Why do we need a copy? It's never mutated?" I will explain the answer with the following example:
struct S {
func someMethod(closure: () -> Void) {
closure()
// ERROR: exclusivity violation
}
mutating func mutate()
}
var s = S()
s.someMethod { s.mutate() }
The closure captures a mutable self and we have access inside the method to the immutable self which means we have two references that alias: one mutable and one not. This violates exclusivity. To fix it, self is copied before each non-mutating method call (if self is mutable).