I'm building a circular linked list, and I would like to know if the do_remove
method is well defined. When I run the program, it shows me that it is, however, I'm still a bit confused why I don't need a virtual destrutor in this case.
Is the virtual destructor only needed when I want to destroy a derived class through it's base pointer?
Does this mean that by downcasting a base class to a derived class, and then calling the derived class destructor, it will always call the base class destructor?
Can there be possible leaks in the do_remove
method?
PS: I need the object creation and destruction to be a two step process - allocation / call to constructor / call to destructor / deallocation; that's why I'm using ::operator new
for the time being.
Here's a self contained example of the code I'm writing:
#include <iostream>
struct NodeBase {
NodeBase * previous;
NodeBase * next;
NodeBase() noexcept
: previous(this)
, next(this) {
}
NodeBase(NodeBase * const previous, NodeBase * const next) noexcept
: previous(previous)
, next(next) {
}
~NodeBase() {
std::puts("~NodeBase()");
}
};
template <typename TYPE>
struct Node : NodeBase {
TYPE data;
template <typename ...ARGUMENTS>
Node(NodeBase * const previous, NodeBase * const next, ARGUMENTS && ...arguments)
: NodeBase(previous, next)
, data(std::forward<ARGUMENTS>(arguments)...) {
previous->next = this;
next->previous = this;
}
~Node() {
std::puts("~Node()");
}
};
template <typename TYPE>
class List {
using Node = Node<TYPE>;
int64_t this_length;
NodeBase this_sentinel;
Node * as_node(NodeBase * const input) noexcept {
return static_cast<Node * const>(input);
}
Node const * as_node(NodeBase const * const input) const noexcept {
return static_cast<Node const * const>(input);
}
template <typename ...ARGUMENTS>
List & do_insert(NodeBase * const node, ARGUMENTS && ...arguments) {
void * const address = ::operator new(sizeof(Node));
try {
new (address) Node(node->previous, node, std::forward<ARGUMENTS>(arguments)...);
++this_length;
return *this;
} catch (...) {
::operator delete(address);
throw;
}
}
// Is this method well defined?
List & do_remove(NodeBase * input) noexcept {
Node * const node = as_node(input);
input->previous->next = input->next;
input->next->previous = input->previous;
node->~Node();
::operator delete(node);
--this_length;
return *this;
}
public:
List()
: this_length(0)
, this_sentinel() {
}
~List() {
std::puts("~List()");
while (this_length) {
pop();
}
}
TYPE & head() noexcept {
return as_node(this_sentinel.next)->data;
}
TYPE const & head() const noexcept {
return as_node(this_sentinel.next)->data;
}
TYPE & last() noexcept {
return as_node(this_sentinel.previous)->data;
}
TYPE const & last() const noexcept {
return as_node(this_sentinel.previous)->data;
}
template <typename ...ARGUMENTS>
List & push(ARGUMENTS && ...arguments) {
return do_insert(this_sentinel.next, std::forward<ARGUMENTS>(arguments)...);
}
List & pop() noexcept {
return do_remove(this_sentinel.next);
}
};
int main() {
List<int> list;
list.push(5).push(7).push(3);
std::cout << list.head() << std::endl;
std::cout << list.last() << std::endl;
return 0;
}
Is the virtual destructor only needed when I want to destroy a derived class through it's base pointer?
Yes. Only if you delete object using base pointer (or if you manage it using unique_ptr
to base) you need virtual destructor in base. Other cases like deleting pointer to most derived type or managing with 'shared_ptr' to base do not need virtual destructor.
Does this mean that by downcasting a base class to a derived class, and then calling the derived class destructor, it will always call the base class destructor?
Yes. Destructor of derived class is required to call destructors of (base and member) sub-objects.
Can there be possible leaks in the do_remove method?
No, if the destructor of TYPE does not leak. For simplicity it would be better to use ordinary delete expression
delete node;
instead of writing out what it is anyway required to do
node->~Node();
::operator delete(node);