C++ destructor called twice for C++ property in Swift class

I have some code like the following:

C++:

class TestCPPClass
{
public:
    TestCPPClass();
    ~TestCPPClass();
private:
    std::vector<int> *vecPtr;
};

TestCPPClass::TestCPPClass()
{
    vecPtr = new std::vector<int>();
}

TestCPPClass::~TestCPPClass()
{
    delete vecPtr;
}

Swift:

class TestCPPWrapper
{
    var impl: TestCPPClass
    
    init() {
        impl = TestCPPClass()
    }
}

When I instantiate a TestCPPWrapper, TestCPPClass's destructor is called twice, but the constructor is only called once. My hunch is that the destructor is called because the old value is being overwritten - but because the destructor call is not paired with a constructor call, I get memory issues because the delete is called with no matching new.

I know I can markup TestCPPClass to adopt reference semantics in Swift- and I'm guessing that will fix my issue. But I was wondering if there is a way around this issue for value types. As I'm learning about Swift/C++ interop I also wanted to make sure I wasn't misunderstanding the patterns or best practices.

Your C++ type is implicitly copyable because it doesn't have any non-copyable fields and the copy constructor isn't deleted, so it's probably getting copied on the Swift side using the default memberwise copy constructor, and then the destructor is running on each of the copies. Writing an explicit copy constructor should help confirm this, and = delete-ing the copy constructor would prevent the type from being copyable as a value type in both C++ and Swift (though I don't think we import noncopyable C++ types into Swift yet).

3 Likes

Thanks! That is indeed the issue. Adding an explicit copy constructor shows what the underlying behavior is and why dealloc is being called. Explicitly deleting the copy constructor causes Swift to become very unhappy.

I'll make a note that as we adopt Swift/C++ interop that (at least for the time being) we should make sure to be careful around classes that have poorly defined copy semantics.

This issue probably would have happened under C++ as well, but in adopting our code most these types were only reference managed. I think Swift's current relationship with C++ reference types out of the box, and the way this is a little different than typical Swift native property initialization is probably what caught me off guard.

In the abstract, I think it would be reasonable for Swift to just be more conservative about classes that define a destructor but don't implement either rule-of-3 or rule-of-5. (Classes that are fine with the implicit copy/move constructor can be explicit about that with = default.)

"Being more conservative" would ideally mean importing it as a non-movable type, not as just a non-copyable type; that's a problem, because we don't have that concept in Swift right now. But importing it as a non-copyable type would at least avoid the most common problems by ensuring that destructor counts match constructor counts.

1 Like

In C++, if you define a copy or move constructor, you don't get a default operator=. This is even true if the copy/move constructor is = default!

class Foo {
public:
  Foo() {}
  Foo(const Foo&) = default;
  Foo(Foo&&) = default;
};

int main() {
  Foo a;
  Foo b;
  a = b;
  // error: object of type 'Foo' cannot be assigned because its copy assignment operator is implicitly deleted
  // note: copy assignment operator is implicitly deleted because 'Foo' has a user-declared move constructor
}

So, I tend to avoid writing = default where I can get away with it, just so that I still have operator=.

1 Like

Sure, and in a type like Foo, we shouldn't need you to default anything. (Although, just so you know, you can also default your operator=s if you want.)

I think that right rule would be something like this: a C++ class type that is being imported as a Swift value type is imported as non-copyable (ideally non-movable) if it does not have consistent value operations. A type is said to have consistent value operations if:

  • all of its value operations are either user-declared or deleted; or if not that,
  • all of its value operations that are not deleted are defined as defaulted, and all of its subobject types have consistent value operations.

So Foo would have consistent value operations because it falls into the second clause, but TestCPPClass would not because the destructor is user-provided but the copy constructor is defaulted (along with all the other value operations).

2 Likes

Was thinking about this problem over the weekend. I think what tripped me up is I did not expect that I was actually performing a copy. I knew it was likely constructor related - and that a constructor was not being called - but I didn't make it to copy constructor in my head.

I'm wondering if the confluence of Swift-isms and C++-isms isn't helping.

  • In C++ - I could call the constructor in place on the member value in the initializer list. There wouldn't be a copy.
  • In Swift - I'm actually not sure how member structs are initialized. My assumption was that they matched C++ behavior and that initializing a struct in a constructor did so in place. Is that the case, or are value type properties copied into on initialization?

Bringing the type into Swift as a reference type seems like the easiest fix - at the cost of needing to modify the C++ source. But it would be nice if it was possible in Swift to match C++ semantics and populate the property without a copy constructor.