Reputation: 298
I am designing a custom C++ template container like this:
template <class C>
class Container {
public:
...
void clear() {
// should I call destructor on elements here?
for (i...)
array[i].~C(); // ?
}
~Container() {
delete [] array_;
}
private:
C* array_;
};
Should I call the destructor on elements manually inside clear()? If I don't, they will be called later when the container is destroyed (since I delete [] array_ in destructor), but this is not the expected behaviour (we expect them to be destroyed right inside clear(), just like std::vector does). However, if I do call the destructors, that memory remains in place and when I will be adding new elements on top of the old ones (which now are destroyed), the assignment operator will be called on those destroyed elements, and this may result in undefined behavior. Suppose I have a class:
class Foo {
public:
Foo() { foo_ = new int; }
~Foo() { delete foo_; } // note I don't explicitly set foo_ to nullptr here
Foo(Foo &&f) {
f.foo_ = std::exchange(foo_, f.foo_); // standard practice in move constructors
}
};
OK, this may look good so far, but now if I make a
Container<Foo> c;
c.add(Foo());
c.clear();
c.add(Foo());
when calling clear() the destructor of the initial Foo is called, leaving its foo_ pointer dangling. Next, when adding the second Foo, the temporary R-value is exchanged with the old contents of the destructed object and when the temp will be destroyed, its destructor will try to delete the same dangling pointer again, which will crash.
So, how to correctly clear() a container without leaving room for double deletion? I also read that its recommended not to set pointers to nullptr in destructor in order not to not hide potential dangling pointer problems.
I'm a little confused about how to approach this.
EDIT:-------------------
There seems to be no compromise-free approach for a template container. So far I see these options, as others pointed out:
Upvotes: 1
Views: 1595
Reputation: 2654
As you specified in the comments, in the unassigned cells you have objects constructed by the default constructor (T()
). That may be bad for performance, but certainly preserves the object abstraction.
Instead of deleting the entries, just replace them with the ones constructed by the default constructor:
template <class C>
class Container {
public:
...
void clear() {
for (i...)
array_[i] = C();
}
~Container() {
delete [] array_;
}
private:
C* array_;
};
On the other hand, I would propose a more performant approach, although it breaks the nice C++ abstraction. You could allocate the memory without invoking the unnecessary default constructor:
template <class C>
class Container {
public:
Container(int n) {
array_ = (C*)malloc(sizeof(C)*n);
size_ = 0;
}
...
void clear() {
for (... i < size_ ...)
array_[i].~C();
size_ = 0;
}
~Container() {
clear();
free(array_);
}
private:
C* array_;
int size_;
};
Here you do call destructors on all the initialized elements, but you do not call it second time, because you keep track of which are initialized, which are not.
Upvotes: 1
Reputation: 36597
The delete [] array_
will invoke the destructor for each element of array_
(assuming C
is a type with a destructor).
Invoking a destructor twice on any given object gives undefined behaviour.
This means your clear()
member function should not directly invoke destructors for the array elements.
If you insist on having a separate clear()
function, which does not interfere with working of the destructor, simply implement it as
void clear()
{
delete [] array_;
array = nullptr; // NULL or 0 before C++11
}
This won't interfere with the destructor since operator delete
has no effect if acting on a NULL pointer.
Upvotes: 3