7
votes

I'm concerned about using an incomplete type with a smart pointer and how the pointer is deleted. Is the following code safe? I don't think it would be, as main.cpp would generate Farm's default destructor which wouldn't see the complete type. To make it safe, I think I should create a non-inline destructor which sees the complete type. Is that correct?

Also is it the same if I used std::vector<Cow> in Farm instead?

farm.h

class Cow;

struct Farm
{
    Farm();
    // ~Farm();
    std::unique_ptr<Cow> cow;
};

farm.cpp

#include "cow.h"
// cow now complete

Farm::Farm()
{
    cow.reset(new Cow);
}

// Farm::~Farm() {}

main.cpp

#include "farm.h"

int main()
{
    Farm farm;
}

Edit: I tried to compile with Visual Studio without the destructor and it says error C2338: can't delete an incomplete type. I guess that answers my question.

2
Note that with auto_ptr, the code compiles :-/Jarod42
@Jarod: Yes, and silently does the wrong thing.Ben Voigt
@Jarod42: With auto_ptr the code is bad, because the default destructor of Farm uses the destructor of auto_ptr, which just does delete with the incomplete type Cow. This has UB if the actual destructor of Cow is non-trivial, and is valid otherwise. Since the compiler can't tell which it is (not having the definition of Cow), it goes ahead and outputs the code.Steve Jessop

2 Answers

4
votes

I don't think your code should compile (and it doesn't in gcc)

std::unique_ptr<Cow> uses std::default_delete<Cow>, and std::default_delete<Cow>::operator() should fail to instantiate for an incomplete type Cow.

See also Is it true that a unique_ptr declaration, unlike a auto_ptr declaration, is well-defined when its template type is of an incomplete type?

So you're right: you need to ensure that default_delete<Cow>::operator() is instantiated somewhere that the Cow type is complete. Which means the destructor of Farm needs to be defined in such a place.

I've just noticed that your subject says "smart pointer", while the question specifies unique_ptr. The answer would be different for a shared_ptr, since std::shared_ptr<Cow>::reset() is a function template that captures the (static) type of the pointer passed to it and stores a deleter. So with shared_ptr all you need is that the call to reset has the complete type Cow -- the location of the destructor doesn't matter.

1
votes

The default destructor for Farm will include a desructor for the unique_ptr, which will include a destructor for Cow. This call will be done even if there's no definition available at the time of compiling. The linker will be expected to connect things up after the fact.