Forward declaring a type in C++: The good, and the bad

In today's post, I would like to shed some light on the implications of forward declaring a type in C++.

The most common pattern I know is when we want to hide the implementation of a class type, we forward declare that class like this:

1
2
3
4
5
6
class Cat;

class Dog
{
  Cat* garfield{};
};

First, the term in the standard is that Cat here is an incomplete type, while people often refer to this as a forward declaration. This pattern means that you can use a pointer or a reference of Cat either as a data member of a class or a parameter to a function or the function return object. For all these use cases, the actual size of a Cat object isn't needed. Member functions of Cat are also not used at this point, so teaching the compiler that a type Cat exists is enough to be able to form pointers or references of an incomplete type.

Why do you want to use an incomplete type?

What is the benefit of code like the above? It allows you to avoid including the header file containing the definition of Cat. Assume you're working with a large codebase; compile times can become a factor. Each header file that you can avoid including helps speed up compile times. That's true even for small projects, but there, this speedup is less of a factor.

Tools like Include What You Use facilitate incomplete types by using the above strategy. The project lists a couple of positive items that avoiding to include a header file brings you.

So what's good about incomplete types? Better compile times as well as a few other things.

Are there drawbacks? Yes!

Book an in-house C++ training class

Do you like this content?

I'm available for in-house C++ training classes worldwide, on-site or remote. Here is a sample list of my classes:
  • From C to C++
  • Programming with C++11 to C++17
  • Programming with C++20
All classes can be customized to your team's needs. Training services

Why do you want to be careful with an incomplete type?

Let's suppose we have code like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
struct Cat;  A 

B 
void Fun(Cat* garfield)
{
  delete garfield;  C 
}

struct Cat {
  D 
  ~Cat() { puts("Cats have seven lives!"); }
};

void Use()
{
  E Please stay away from raw pointers, this is just for demonstration purposes
  Fun(new Cat{});
}

The class, or better struct Cat is forward declared A, before a function Fun is declared, which takes a Cat pointer as parameter. Inside Fun all that's done is using delete on that parameter C.

Next, the definition for the class Cat is provided. For the sake of this example, all that's needed is the nice destructor D, which states that cats have more than one life. Let's see about that.

If you look further down, you can see that in E a call to Fun takes place, creating a Cat object with new.

At this point, in one of my training classes, I would ask you what you think is the output of this program. So, what do you think? I give you the time it takes you to scroll down one page to think about this.

Be prepared, here it is:

1

Yes, nothing! Well, your compiler has some words to say:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<source>: In function 'void Fun(Cat*)':
<source>:7:3: warning: possible problem detected in invocation of 'operator delete' [-Wdelete-incomplete]
    7 |   delete t;
      |   ^~~~~~~~
<source>:5:15: warning: 't' has incomplete type
    5 | void Fun(Cat* t)
      |          ~~~~~^
<source>:3:8: note: forward declaration of 'struct Cat'
    3 | struct Cat;
      |        ^~~
<source>:7:3: note: neither the destructor nor the class-specific 'operator delete' will be called, even if they are declared when the class is defined
    7 |   delete t;
      |   ^~~~~~~~

What just happened is that you hit undefined behavior. At C, the compiler doesn't know whether Cat has a trivial destructor or not, and it also doesn't know whether it will ever know better. What it does is assume that Cat has a trivial destructor. When the non-trivial destructor for Cat is later defined, it's too late. The code for Fun has already been generated.

Once you move the definition of Cat before Fun you get the desired output:

1
Cats have seven lives!

Well, now it did cost the cat one life. But there are plenty left.

The takeaway from here, be sure that you use delete only with a complete type.

Oh, how you can ensure that, you're asking?

That is a little tricky because the language explicitly allows incomplete types. An operation that requires a complete type and does not silently do something unexpected is sizeof. Wrap it in a static_assert like below in A and you're fine:

1
2
3
4
5
6
void Fun(Cat* garfield)
{
  A 
  static_assert(sizeof(Cat) >= 0, "Cannot delete an incomplete type");
  delete garfield;
}

The STL has you covered. For example, std::is_trivially_destructible_v only compiles with a complete type, and so does std::unique_ptr.

If you compile my initial code with Clang 19 or above using C++26, you will get a compile error. The reason is that the wording was updated in C++26 to avoid the UB here, thanks to P3144R0.

All right, enough paper cuts? Sure, then stay tuned for next months post.

Summary

Using incomplete types to forward declare classes is a great way to reduce compile times. However, it also opens the door for undefined or at least surprising behavior. Be extra careful when forward-declaring types.

[Update 10. Oct. 2025]: As Daniela Engert pointed or function return object was missing.

Andreas

Recent posts