Referring to objects of incomplete class type, also known as forward declarations, is a common practice. One such common usage is with the "pimpl idiom" [Sutter 00] whereby an opaque pointer is used to hide implementation details from a public-facing API. However, attempting to delete a pointer to an object of incomplete class type can lead to undefined behavior. The C++ Standard, [expr.delete], paragraph 5 [ISO/IEC 14882-2014], states the following:
If the object being deleted has incomplete class type at the point of deletion and the complete class has a non-trivial destructor or a deallocation function, the behavior is undefined.
Do not attempt to delete a pointer to an object of incomplete type. Although it is well-formed if the class has no nontrivial destructor and no associated deallocation function, it would become undefined behavior were a nontrivial destructor or deallocation function added later. It would be possible to check for a nontrivial destructor at compile time using a static_assert
and the std::is_trivially_destructible
type trait, but no such type trait exists to test for the presence of a deallocation function.
...
reinterpret_cast
of a pointer type is defined by [expr.reinterpret.cast], paragraph 7, as being static_cast<cv T *>(static_cast<cv void *>(PtrValue))
, meaning that reinterpret_cast
is simply a sequence of static_cast
operations. C-style casts of a pointer to incomplete an incomplete object type are defined as using either static_cast
or reinterpret_cast
(it is unspecified which is picked is unspecified) in [expr.cast], paragraph 5.
...
In this noncompliant code example, a class attempts to implement the pimpl idiom but deletes a pointer to an incomplete class type, resulting in undefined behavior if Body
has a nontrivial destructor:.
Code Block | ||||
---|---|---|---|---|
| ||||
class Handle { class Body *Implimpl; // Declaration of a pointer to an incomplete class public: ~Handle() { delete Implimpl; } // Deletion of pointer to an incomplete class // ... }; |
...
In this compliant solution, the deletion of Impl
impl
is moved to a part of the code where Body
is defined:.
Code Block | ||||
---|---|---|---|---|
| ||||
class Handle { class Body *Implimpl; // Declaration of a pointer to an incomplete class public: ~Handle(); // ... }; // Elsewhere class Body { /* ... */ }; Handle::~Handle() { delete Implimpl; } |
Compliant Solution (std::shared_ptr
)
In this compliant solution, a std::shared_ptr
is used to own the memory to Impl
impl
. Note that a A std::shared_ptr
is capable of referring to an incomplete type, but a std::unique_ptr
is not.
Code Block | ||||
---|---|---|---|---|
| ||||
#include <memory> class Handle { std::shared_ptr<class Body> Implimpl; public: Handle(); ~Handle() {} // ... }; |
...
Pointer downcasting (casting a pointer to a base class into a pointer to a derived class) may require adjusting the address of the pointer by a fixed amount that can be determined only when the layout of the class inheritance structure is known. In this noncompliant code example, f()
retrieves a polymorphic pointer of complete type B
from getDget_d()
. That pointer is then cast to a pointer of incomplete type D
before being passed to g()
. Casting to a pointer to the derived class may fail to properly adjust the resulting pointer, causing undefined behavior when the pointer is dereferenced by calling d->doSomething>do_something()
.
Code Block | ||||
---|---|---|---|---|
| ||||
// File1.h class B { protected: double d; public: B() : d(1.0) {} }; // File2.h void g(class D *); class B *getDget_d(); // Returns a pointer to a D object // File1.cpp #include "File1.h" #include "File2.h" void f() { B *v = getDget_d(); g(reinterpret_cast<class D *>(v)); } // File2.cpp #include "File2.h" #include "File1.h" #include <iostream> class Hah { protected: short s; public: Hah() : s(12) {} }; class D : public Hah, public B { float f; public: D() : Hah(), B(), f(1.2f) {} void doSomethingdo_something() { std::cout << "f: " << f << ", d: " << d << ", s: " << s << std::endl; } }; void g(D *d) { d->doSomething>do_something(); } B *getDget_d() { return new D; } |
Implementation Details
When compiled with ClangBB. Definitions#clang3.58 and the function f()
is executed, the noncompliant code example prints the following:.
Code Block |
---|
f: 1.89367e-40, d: 5.27183e-315, s: 0 |
Similarly, unexpected values are printed when the example is run in Microsoft Visual Studio 2013 2015 and GCC 46.91.0.
Compliant Solution
This compliant solution assumes that the intent is to hide implementation details by using incomplete class types. Instead of requiring a D *
to be passed to g()
, it expects a B *
type:.
Code Block | ||||
---|---|---|---|---|
| ||||
// File1.h class B { protected: double d; public: B() : d(1.0) {} }; -- contents identical. // File2.h void g(class B *); // Accepts a B object, expects a D object class B *getDget_d(); // Returns a pointer to a D object // File1.cpp #include "File1.h" #include "File2.h" void f() { B *v = getDget_d(); g(v); } // File2.cpp #include// "File2.h" #include "File1.h" #include <iostream> class Hah { protected: short s; public: Hah() : s(12) {} }; class D : public Hah, public B { float f; public: D() : Hah(), B(), f(1.2f) {} void doSomething() { std::cout << "f: " << f << ", d: " << d << ", s: " << s << std::endl; } }; void ... all contents are identical until ... void g(B *d) { D *t = staticdynamic_cast<D *>(d); if (t) { t->doSomething>do_something(); } else { // Handle error } } B *getDget_d() { return new D; } |
Risk Assessment
Casting pointers or references to incomplete classes can result in bad addresses. Deleting a pointer to an incomplete class results in undefined behavior if the class has a nontrivial destructor. Doing so can cause program termination, a runtime signal, or resource leaks.
Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
---|---|---|---|---|---|
EXP57-CPP | Medium | Unlikely | Medium | P4 | L3 |
Automated Detection
Tool | Version | Checker | Description | ||||||
---|---|---|---|---|---|---|---|---|---|
Astrée |
| delete-with-incomplete-type | |||||||
Coverity | 6.5 | DELETE_VOID | Fully implemented | ||||||
Clang |
| -Wdelete-incomplete |
CodeSonar |
| LANG.CAST.PC.INC | Conversion: pointer to incomplete | ||||||
Helix QAC |
| C++3112 | |||||||
Klocwork |
| CERT.EXPR.DELETE_PTR.INCOMPLETE_TYPE | |||||||
LDRA tool suite |
| 169 S, 554 S | Enhanced Enforcement | ||||||
Parasoft C/C++test |
| CERT_CPP-EXP57-a | Do not delete objects with incomplete class at the point of deletion |
Parasoft Insure++ |
Runtime detection | |||||||||
Polyspace Bug Finder |
| CERT C++: EXP57-CPP | Checks for conversion or deletion of incomplete class pointer | ||||||
RuleChecker |
| delete-with-incomplete-type |
Related Vulnerabilities
Search for vulnerabilities resulting from the violation of this rule on the CERT website.
Bibliography
[Dewhurst |
2002] | Gotcha #39, "Casting Incomplete Types" |
[ISO/IEC 14882-2014] | Subclause 4.10, "Pointer Conversions" |
[Sutter |
2000] | "Compiler Firewalls and the Pimpl Idiom" |
...
...