Home > Articles > Programming > C/C++

C++ Reference Guide

Hosted by

Toggle Open Guide Table of ContentsGuide Contents

Close Table of ContentsGuide Contents

Close Table of Contents

More on Declarations and Definitions

Last updated Jan 1, 2003.

In my previous column about declarations and definitions, I briefly mentioned the issue of declaring a type without defining it. Today, I explain this topic, and I show how C++ lets you use a type that hasn't been defined yet, in contexts of which most programmers are unaware.

Type Declaration vs. Definition

As I explained previously, the terms declaration and definition mean different things when they apply to an object, as opposed to when they apply to a type or a function. In the former case, a definition binds a name (i.e., an object) to a type and causes the compiler to allocate storage for that object; whereas a declaration merely binds a name to a type without allocating storage for it:

extern int signum; //declaration, definition appears elsewhere
 int main()
{}

struct S
{
static int y;//declaration
};
int S::y=10; //definition

However, in the case of functions and types, a definition contains a body:

int func(int *p) //function definition
{
 return 0;
}
class X //class definition
{
int val;
public:
X();
~X();
int getval() const; 
};

Incomplete Declarations

Definitions of this kind are also declarations. However, the reverse isn't necessarily true. There are declarations of non-object entities (functions, templates, classes, enum types, etc.) that aren't definitions:

//declarations but not definitions:
int func(int *p); 
class X; 
struct Y;
template <class T> class Z;
union U;
enum Dir;

Declarations of this kind are called function prototypes (when referring to functions and function templates) or forward declarations (when applied to classes, structs, unions, enum types and class templates). Collectively, they're called incomplete declarations.

C++ doesn't let you use an identifier without defining it first (or at least declaring it first). An incomplete declaration is, therefore, a means of pacifying the compiler in cases like these:

int func(int *);
class X;
union U;

struct A
{
 void init() {int stat= func(&param)}; 
private:
 X * px;
 U & myunion; 
};

Most programmers know that a forward declaration allows them to create a pointer or a reference to a certain type, as shown in struct A above. However, you can do much more with it:

class C;
struct B
{
static C c; //fine
};

B contains a static value member of type C. This type wasn't defined before, only forward-declared. Still, you can use a forward declaration to create a static value member. The reason is that the static member c was only declared inside B, not defined. A static data member must also be defined outside the class body, and this definition does require that C be fully defined beforehand:

#include "C.h"
C B::c; //definition of a static data member

Now what about the following definition of class Thread?

class L;
class Thread
{
public:
 L join(L l);
 void suspend(L l);
};

There are no typos in this code: Thread's member function take and return L objects by value, although L was only forward-declared. Is this legal? Yes, it is. As long as join() and suspend() are only declared, their return value and parameters -- whatever these may be -- needn't be defined. If, however, you define any of Thread's member functions inline:

class Thread
{
public:
 L join(L l);
 void suspend(L l) { __sys_suspend_thr(&l); } //error 
};

You'll get a compilation error about using an incomplete type L. We can conclude that forward declarations aren't limited to pointers and references; you can also use them for value objects: static data members or a class, a function's return type or a parameter etc., so long as these value objects appear in declarations, too.

Surprisingly, certain compilers allow you to even define member functions that use value objects in their return type and parameter list. For example, C++ BuilderX accepts the following code:

class Thread
{
public:
 L join(L l);
 void suspend(L l) { int x=0; x++; } //accepted by C++ BuilderX
};

It isn't hard to tell why the compiler has no problem with the definition of suspend(); the function's body doesn't refer to type L in any way, so the compiler can process it blissfully. However, this behavior isn't standard compliant. Comeau, for instance, rejects this code:

"ComeauTest.c", line 6: error: incomplete type is not allowed
  void suspend(L l) { int x=0; x++; }

Conclusions

An incomplete declaration is more useful than what may seem at first. If a class definition contains only declarations of member functions, and pointer and reference data members, you don't need to define other types before using them. This means that you can remove many #includes. Consider the following example:

// ++file Thread.h++

#include "L.h"
#include "M.h"

class Thread
{
public:
 L join(L l) {..}
 void suspend(L l) { int x=0; x++; } 
 int count (M m) const;
};

Replacing inline function definitions with function prototypes would allow you to remove the two #includes from Thread.h:

class L;
class M;
class Thread
{
public:
 L join(L l);
 void suspend(L l);
 int count (M m) const;
};

This way, you can reduce compilation time significantly and minimize compile-time dependencies.