CertC++-OOP52¶
Do not delete a polymorphic object without a virtual destructor
Required inputs: IR
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 publicmember 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 |
Related Guidelines
| SEI CERT C++ Coding Standard | EXP51-CPP. Do not delete an array through a pointer of the incorrect type |
Bibliography
| [ ISO/IEC 14882-2014] | Subclause 5.3.5, "Delete" Subclause 12.4, "Destructors" |
| [ Stroustrup 2006] | "Why Are Destructors Not Virtual by Default?" |
Possible Messages
Key |
Text |
Severity |
Disabled |
|---|---|---|---|
base_class_destructor |
Destructor of a base class with existing delete operations through pointers-to-base shall be virtual. |
None |
False |
delete_through_pointer_to_base |
Delete operations through pointer-to-base should only be used with classes that have virtual destructors. |
None |
False |
polymorphic_class_destructor |
Destructor of a polymorphic base class shall be virtual or protected non-virtual. |
None |
False |
Options¶
This rule shares the following common options: exclude_in_macros, exclude_messages_in_system_headers, excludes, extend_exclude_to_macro_invocations, includes, justification_checker, languages, post_processing, provider, report_at, severity
The following places define options that affect this rule: Stylechecks, Analysis-GlobalOptions
allowed_inheriting_classes¶
allowed_inheriting_classes
A set of full qualified type names that are ignored when determining whether a class is a base class or not. This is mainly a workaround for some libc++ implementation details: The standard library uses private inheritance from empty base classes to implement EBO (Empty Base Optimization) for some of its data structures.Type: set[bauhaus.analysis.config.QualifiedName]
Default:
{'std::_Compressed_pair', 'std::__compressed_pair_elem', 'std::__detail::_Hashtable_ebo_helper', 'std::__libcpp_compressed_pair_imp', 'std::__map_value_compare', 'std::__unordered_map_hasher'}