The C++ Standard, [expr.delete], paragraph 3 [ISO/IEC 14882-2014], states the following:

In the first alternative (delete object), if the static type of the object to be deleted is different from its dynamic type, the static type shall be a base class of the dynamic type of the object to be deleted and the static type shall have a virtual destructor or the behavior is undefined. In the second alternative (delete array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.

Do not delete an object of derived class type through a pointer to its base class type that has a non-virtual destructor. Instead, the base class should be defined with a virtual destructor. Deleting an object through a pointer to a type without a virtual destructor results in undefined behavior.

Noncompliant Code Example

In this noncompliant example, b is a polymorphic pointer type whose static type is Base * and whose dynamic type is Derived *. When b is deleted, it results in undefined behavior because Base does not have a virtual destructor. The C++ Standard, [class.dtor], paragraph 4 [ISO/IEC 14882-2014], states the following:

If a class has no user-declared destructor, a destructor is implicitly declared as defaulted. An implicitly declared destructor is an inline public member of its class.

The implicitly declared destructor is not declared as virtual even in the presence of other virtual functions.

struct Base {
  virtual void f();
};
 
struct Derived : Base {};
 
void f() {
  Base *b = new Derived();
  // ...
  delete b;
}

Noncompliant Code Example

In this noncompliant example, the explicit pointer operations have been replaced with a smart pointer object, demonstrating that smart pointers suffer from the same problem as other pointers. Because the default deleter for std::unique_ptr calls delete on the internal pointer value, the resulting behavior is identical to the previous noncompliant example.

#include <memory>
 
struct Base {
  virtual void f();
};
 
struct Derived : Base {};
 
void f() {
  std::unique_ptr<Base> b = std::make_unique<Derived()>();
}

Compliant Solution

In this compliant solution, the destructor for Base has an explicitly declared virtual destructor, ensuring that the polymorphic delete operation results in well-defined behavior.

struct Base {
  virtual ~Base() = default;
  virtual void f();
};

struct Derived : Base {};

void f() {
  Base *b = new Derived();
  // ...
  delete b;
}

Exceptions

OOP52-CPP:EX0: Deleting a polymorphic object without a virtual destructor is permitted if the object is referenced by a pointer to its class, rather than via a pointer to a class it inherits from.

class Base {
public:
  // ...
  virtual void AddRef() = 0;
  virtual void Destroy() = 0;
};

class Derived final : public Base {
public:
  // ...
  virtual void AddRef() { /* ... */ }
  virtual void Destroy() { delete this; }
private:
  ~Derived() {}
};

Note that if Derived were not marked as final, then delete this could actually reference a subclass of Derived, violating this rule.

OOP52-CPP:EX1: Deleting a polymorphic object without a virtual destructor is permitted if its base class has a destroying operator delete that will figure out the correct derived class's destructor to call by other means.

#include <new>

class Base {
  const int whichDerived;

protected:
  Base(int whichDerived) : whichDerived(whichDerived) {}

public:
  Base() : Base(0) {}
  void operator delete(Base *, std::destroying_delete_t);
};

struct Derived1 final : Base {
  Derived1() : Base(1) {}
};

struct Derived2 final : Base {
  Derived2() : Base(2) {}
};

void Base::operator delete(Base *b, std::destroying_delete_t) {
  switch (b->whichDerived) {
  case 0:
    b->~Base();
    break;
  case 1:
    static_cast<Derived1 *>(b)->~Derived1();
    break;
  case 2:
    static_cast<Derived2 *>(b)->~Derived2();
  }
  ::operator delete(b);
}

void f() {
  Base *b = new Derived1();
  // ...
  delete b;
}


Risk Assessment

Attempting to destruct a polymorphic object that does not have a virtual destructor declared results in undefined behavior. In practice, potential consequences include abnormal program termination and memory leaks.

Rule

Severity

Likelihood

Remediation Cost

Priority

Level

OOP52-CPP

Low

Likely

Low

P9

L2

Automated Detection

Tool

Version

Checker

Description

Astrée

22.10

non-virtual-public-destructor-in-non-final-class
Partially checked
Axivion Bauhaus Suite

7.2.0

CertC++-OOP52
Clang
3.9
-Wdelete-non-virtual-dtor
CodeSonar
8.1p0

LANG.STRUCT.DNVD

delete with Non-Virtual Destructor

Helix QAC

2024.1

C++3402, C++3403, C++3404


Klocwork
2024.1

CL.MLK.VIRTUAL
CWARN.DTOR.NONVIRT.DELETE


LDRA tool suite
9.7.1

 

303 S

Partially implemented

Parasoft C/C++test
2023.1

CERT_CPP-OOP52-a

Define a virtual destructor in classes used as base classes which have virtual functions

Polyspace Bug Finder

R2023b

CERT C++: OOP52-CPPChecks for situations when a class has virtual functions but not a virtual destructor (rule partially covered)
PVS-Studio

7.30

V599, V689
RuleChecker
22.10
non-virtual-public-destructor-in-non-final-class
Partially checked
SonarQube C/C++ Plugin
4.10
S1235

Related Vulnerabilities

Search for other vulnerabilities resulting from the violation of this rule on the CERT website.

Related Guidelines

Bibliography

[ISO/IEC 14882-2014]

Subclause 5.3.5, "Delete"
Subclause 12.4, "Destructors" 

[Stroustrup 2006]"Why Are Destructors Not Virtual by Default?"



6 Comments

  1. its perhaps worth noting this example that is safe

    class Interface

    public:

      virtual void Foo() = 0;

      virtual void AddRef() = 0;

      virtual void Release() = 0;

    };

     

    class Derived final : public Interface

    public:

      virtual void AddRef() 

    Unknown macro: { ... }

    virtual void Release()

    Unknown macro: { delete this; }

    virtual void Foo()

    Unknown macro: { ... }

    private:
    ~Derived() {}

    };

     

    this would be unsafe if Derived wasn't final, but if it is final in a sense the delete this in Release() is not actually on a polymorphic object because we know it must actually be a Derived and not a subclass of Derived.

    both gcc and clang don't warn with Wdeletenon-virtual-dtor for this code because of this reasoning, but other tools may still warn.

    As the naming may suggest this can come up frequently in some schemes that use intrusive reference counting.

     

     

    1. I agree that this code sample does not violate the rule, because you don't delete a Derived through a base class pointer here. (This could still be done elsewhere in the program however.)

      Still, I added an exception to the rule to reflect this.

      BTW, please use the  \{ code \} tag to contain code examples, as Confluence interprets anything in braces as some sort of macro call.

      1. yeah, the destructor being private is pretty important.  I think that means the only ways for someone to violate the rule and delete a derived through a base pointer is to do it in something marked as a friend of Derived, or #define private.  The former seems odd, but maybe there is a case where it is reasonable.  I'm not sure there is a rule against defining keywords to be macros, but maybe there should be, and it certainly seems like something that should be avoided.

      2. I think the new exception is reasonable, but "precise type" is not very descriptive. I think what you're trying to get at is the dynamic type and static type of the pointer value denote the same type.

        1. Tweaked the wording of the exception. We can speak of classes not types since we are addressing polymorphic objects.