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"
Excerpt from SEI CERT C++ Coding Standard [https://cmu-sei.github.io/secure-coding-standards/sei-cert-cpp-coding-standard/rules/expressions-exp/exp57-cpp], Copyright (C) 1995-2026 Carnegie Mellon University. See section 9.4. "3rd-Party Licenses" in the documentation for full details.

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

allowed_conversion_kinds

allowed_conversion_kinds

Type: set[Conversion_Kind]

Default: {'DYNAMIC_CAST'}

If certain conversion kinds (e.g., dynamic casts) should be allowed and not be reported by this rule.
 

message_predicate

message_predicate : typing.Callable[[Cafe_Message], bool] | None = None

If provided, a custom predicate to filter relevant messages. Receives the message node and should return True for messages to report.
 

reported_messages

reported_messages : set[int] | None = {414}

If provided, only messages of these types are reported.
 

reported_severities

reported_severities : set[str] = {'error', 'remark', 'warning'}

List of severities to display.
 

use_error_number

use_error_number : bool = False

Whether the error number from the frontend should be used.
 

use_rule_severity

use_rule_severity : bool = True

Whether the rule's severity or the compiler's severity should be used.
 

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