An incomplete type is a type that has been declared, but its definition hasn’t been seen by the compiler. From the standard [Types / 3.9 note 5]:
“A class that has been declared but not defined, an enumeration type in certain contexts (7.2), or an array of unknown size or of incomplete element type, is an incompletely-defined object type.45 Incompletely-defined object types and the void types are incomplete types (3.9.1). Objects shall not be defined to have an incomplete type.”
To summarize, you can’t define an object of an incomplete type (or dereference a pointer to one). Footnote 45 in the quotation from the standard above gives a hint as to why:
“The size and layout of an instance of an incompletely-defined object type is unknown.”
The compiler knows to allocate enough space for a pointer type, an address in memory, but wouldn’t know how much space to allocate for the incomplete type since its size is yet to be defined.
Probably the most famous incomplete type is void. Void is a fundamental type which is used in a few specific tasks, such as declaring a function which doesn’t return a value, or a parameterless function:
int ParameterlessFunction(void) { ... }
But you already knew all that. What’s more important is that void is an incomplete type. And unlike Jerry MaGuire, can never be made complete. In our case, because it’s prevented by the compiler. Apologies for the pop culture reference. Although probably shouldn’t make any references, since you can’t have a reference to a void.
What can you do with an incomplete type? Not tons. But you can use them to declare pointers. The important part of declaring an incomplete class type, is just that. You’re declaring that class name to be a class. In the same way you can have a void pointer, you can have a pointer to your new type.
class Incomplete; Incomplete* myPtr;
One of the other few things you can do with an incomplete type, is be used as a template type argument. This is of some significance since it allows us to use them with smart pointers, for example: unique_ptr
Incomplete types are probably most famous for the pointer to impl idiom, aka pimpl idiom. In this case we define an interface in the header file and a pointer to an implementation type (impl). The impl type contains the guts of the type and is then defined in the cpp file. For users of the interface defined in the header file, the advantage here is they don’t need to recompile when the impl is modified. For impls requiring many includes, this can also result in faster compile times when the includes can be added to the cpp file instead of the header file (assuming it’s a core file often referenced).
// Header file, should require minimal include. class Incomplete { // Class interface. … class IncompleteImpl; unique_ptr<IncompleteIm> implPtr; }; // cpp file class ImcompleteImp { // define class }; Incomplete::Incomplete() : implPtr( new ImcompleteImp() ) { // define class } // define rest of the class methods...
Other than the benefits outlined above, it does make for a cleaner header file which only expose the necessary interface to the user. I don’t want to get too deeply into the pimpl idiom as it’s not the main focus. For a more in-depth discussion you can read here.
Final thoughts:
Ultimately what we have with incomplete types are essentially void pointers. As we’ve seen, you can’t do much with them. You can’t even dereference an incomplete type. But they can be quite useful to pass around a pointer to a type, while preserving the type safety of C++. Which saves us from working with void*. PS: Keep in mind there’s a bit more around incomplete types and templates, specifically around destructors.