CertC++-EXP57¶
Do not cast or delete pointers to incomplete classes
Required inputs: IR
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.
Pointer downcasting to a pointer of incomplete class type has similar caveats.
Pointer upcasting (casting from a more derived type to a less derived type) is
a standard implicit conversion operation. C++ allows
static_cast to perform the inverse operation, pointer
downcasting, via [expr.static.cast], paragraph 7. However, when the
pointed-to type is incomplete, the compiler is unable to make any class offset
adjustments that may be required in the presence of multiple inheritance,
resulting in a pointer that cannot be validly dereferenced.
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
an incomplete object type are defined as using either
static_cast or
reinterpret_cast (it is unspecified which is picked)
in [expr.cast], paragraph 5.
Do not attempt to cast through a pointer to an object of incomplete type. The cast operation itself is well-formed, but dereferencing the resulting pointer may result in undefined behavior if the downcast is unable to adjust for multiple inheritance.
Noncompliant Code Example
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.
class Handle {
class Body *impl; // Declaration of a pointer to an incomplete class
public:
~Handle() { delete impl; } // Deletion of pointer to an incomplete class
// ...
};
Compliant Solution (
delete)
In this compliant solution, the deletion of
impl is moved to a part of the code where
Body is defined.
class Handle {
class Body *impl; // Declaration of a pointer to an incomplete class
public:
~Handle();
// ...
};
// Elsewhere
class Body { /* ... */ };
Handle::~Handle() {
delete impl;
}
Compliant Solution (
std::shared_ptr)
In this compliant solution, a
std::shared_ptr is used to own the memory to
impl. A
std::shared_ptr is capable of referring to an incomplete type, but
a
std::unique_ptr is not.
#include <memory>
class Handle {
std::shared_ptr<class Body> impl;
public:
Handle();
~Handle() {}
// ...
};
Noncompliant Code Example
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
get_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->do_something().
// File1.h
class B {
protected:
double d;
public:
B() : d(1.0) {}
};
// File2.h
void g(class D *);
class B *get_d(); // Returns a pointer to a D object
// File1.cpp
#include "File1.h"
#include "File2.h"
void f() {
B *v = get_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 do_something() { std::cout << "f: " << f << ", d: " << d << ", s: " << s << std::endl; }
};
void g(D *d) {
d->do_something();
}
B *get_d() {
return new D;
}
Implementation Details
When compiled with
Clang
BB.
Definitions#clang3.8 and the function
f() is executed, the noncompliant code example prints the
following.
f: 1.89367e-40, d: 5.27183e-315, s: 0
Similarly, unexpected values are printed when the example is run in Microsoft Visual Studio 2015 and GCC6.1.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.
// File1.h -- contents identical.
// File2.h
void g(class B *); // Accepts a B object, expects a D object
class B *get_d(); // Returns a pointer to a D object
// File1.cpp
#include "File1.h"
#include "File2.h"
void f() {
B *v = get_d();
g(v);
}
// File2.cpp
// ... all contents are identical until ...
void g(B *d) {
D *t = dynamic_cast<D *>(d);
if (t) {
t->do_something();
} else {
// Handle error
}
}
B *get_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 |
Bibliography
| [ Dewhurst 2002] | Gotcha #39, "Casting Incomplete Types" |
| [ ISO/IEC 14882-2014] | Subclause 4.10, "Pointer Conversions" Subclause 5.2.9, "Static Cast" Subclause 5.2.10, "Reinterpret Cast" Subclause 5.3.5, "Delete" Subclause 5.4, "Explicit Type Conversion (Cast Notation)" |
| [ Sutter 2000] | "Compiler Firewalls and the Pimpl Idiom" |
Possible Messages
Key |
Text |
Severity |
Disabled |
|---|---|---|---|
cafe_message |
{} |
None |
False |
cast_from_polybase_to_derived |
Cast from polymorphic base class to derived class |
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_conversion_kinds¶
allowed_conversion_kinds
If certain conversion kinds (e.g., dynamic casts) should be allowed and not be reported by this rule.Type: set[Conversion_Kind]
Default:
{'DYNAMIC_CAST'}
message_predicate¶
message_predicate : typing.Callable[[Cafe_Message], bool] | None = None
True for messages to
report.
reported_messages¶
reported_messages : set[int] | None = {414}
reported_severities¶
reported_severities : set[str] = {'error', 'remark', 'warning'}
use_error_number¶
use_error_number : bool = False
use_rule_severity¶
use_rule_severity : bool = True
Option Types¶
These types are used by options listed above:
Conversion_Kind¶
An enumeration.UNKNOWN
IDENTITY
QUALIFIER
DERIVED_TO_BASE
BASE_TO_DERIVED
DYNAMIC_CAST
POINTER_TO_MEMBER_DERIVED_TO_BASE
POINTER_TO_MEMBER_BASE_TO_DERIVED
BOOL
DISCARD
NUMERIC
VECTOR_FILL
REINTERPRET_CAST
NULL_POINTER
ARRAY_TO_POINTER
FUNCTION_TO_POINTER
MANAGED_BOXING
MANAGED_UNBOXING