Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

CDT/Obsolete/C editor enhancements/Include management/Forward declarations

< CDT‎ | Obsolete‎ | C editor enhancements‎ | Include management
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

This is a problem page. Please treat it as a discussion page and feel free to insert your comments anywhere. Mathiaskunter.gmail.com 09:16, 3 May 2012 (UTC)

Problem

When organizing includes, we often only need a simple forward declaration instead of a full definition. The intent of this page is to summarize when exactly a forward declaration can be used in C/C++ and when not.

Variables, parameters and return types

Definitions

Variable, parameter and return type definitions generally require the definition of the underlying type:

class X;
X foo(X x1)			// Error!
{
	X x2;			// Error!
}

Definitions of pointers and references however generally only require the declaration of the underlying type:

class X;
X *foo(X *x1, X &x2)		// OK
{
	X *x3;			// OK
	X &x4 = x2;		// OK
}

All other variable, parameter and return type definitions require the definition of the underlying type.

Declarations

Declarations of static member variables also only require the declaration of the underlying type:

class X;
class Y { static X x; };	// OK

Parameter and return type declarations always only require the declaration of the underlying type:

class X;
X foo(X x);			// OK

Expressions

Identifier expressions

Identifier expressions need the definition of the underlying type if they're dereferencing the value of the variable:

class X;
void foo(X &x)
{
	if (x) { }		// Error!
	while (x) { }		// Error!
}

Some identifier expressions however don't dereference the value. Those only need the declaration of the underlying type instead:

class X;
X &foo(X &x)
{
	x;			// OK
	for (x; ; x) { }	// OK
	return x;		// OK
}

If the underlying type is a pointer type, we only require the declaration of the underlying type:

class X;
void foo(X *x)
{
	if (x) { }		// OK
	while (x) { }		// OK
}

Unary operation expressions

Unary operation expressions generally require the definition of the underlying type of the operand. The ampersand operator is an exception to this rule, which only requires the declaration of the underlying type of the operand instead:

class X;
void foo(X &x)
{
	&x;			// OK – ampersand operator
}

Furthermore, if the underlying type of the operand is a pointer type, some operations also only require the declaration of the underlying type of the operand:

#include <typeinfo>
class X;
void foo(X *x)
{
	__alignof(x);		// OK - alignof operator
	!x;			// OK - not operator
	+x;			// OK - unary plus operator
	sizeof(x);		// OK - sizeof operator
	typeid(x);		// OK - typeid operator
	typeof(x) x2;		// OK - typeof operator
}

All other unary operation expressions require the definition of the underlying type of the operand.

Binary operation expressions

Binary operation expressions generally require the definition of the underlying types of both operands. However, if the underlying types of both operands are pointer types and if those pointer types are identical, some operations only require the declaration of the underlying type of the operands instead:

class X;
void foo(X *x)
{
	x = x;			// OK - assignment operator
	x == x;			// OK - equals operator
	x != x;			// OK - not equals operator
	x >= x;			// OK - greater equal operator
	x > x;			// OK - greater operator
	x <= x;			// OK - less equal operator
	x < x;			// OK - less operator
	x && x;			// OK – logical and operator
	x || x;			// OK – logical or operator
}

If the underlying types of both operands are pointer types, but if those pointer types are not identical, only the logical operations settle for the declaration of the underlying types of the operands:

class X;
class Y;
void foo(X *x, Y *y)
{
	x && y;			// OK – logical and operator
	x || y;			// OK – logical or operator
}

All other binary operation expressions require the definitions of the underlying types of both operands.

Function call expressions

Function name expression

Function calls generally only require the declaration of the function at the time it should be called:

void foo();
void bar()
{
	foo();			// OK
}

Exceptions are constructor calls which always require the definition of the underlying type which should be constructed:

class X;
void foo()
{
	X();			// Error!
}

Return and parameter types

The return type / parameter types of a called function must generally be defined at the time the function should be called:

class X;
X foo(X x);
void bar(X &x)
{
	foo(x);			// Error!
}

However, if the return / parameter types of that function are pointer or reference types, only the declaration is required:

class X;
X &foo(X &x);
void bar(X &x)
{
	foo(x);			// OK
}
class X;
X *foo(X *x);
void bar(X *x)
{
	foo(x);			// OK
}

Furthermore, note that the definitions of the underlying types are required if the declared return / parameter type doesn't match the actual return / parameter type (polymorphic types, implicit type casting):

class Base;
class Derived;
Base *foo(Base *b);
void bar(Derived *d)
{
	foo(d);			// Error!
}

Field references

Field references always require the definition of the underlying type of the field which should be referenced:

class X;
void foo(X &x)
{
	x.someField;		// Error!
	x.someFunc();		// Error!
}
class X;
void foo(X *x)
{
	x->someField;		// Error!
	x->someFunc();		// Error!
}

New and delete

The new operator requires a definition of the underlying type:

class X;
void foo()
{
	new X();		// Error!
}

The delete operator only requires a declaration of the underlying type, but this has the side effect that the destructor of this type won’t be called. So we actually need the definition of the underlying type in order to be on the safe side.

class X;
void foo(X *x)
{
	delete x;		// No error, but side effects!
}

Throw and catch

Both throw and catch require a definition of the underlying type which should be thrown or caught. This is even true when pointer or reference types are thrown or caught!

class X;
void foo()
{
	try { }
	catch (X &x) { }	// Error!
}

Qualified names

Referring to composite types

Qualified names where the base name refers to composite types always require a definition of that composite type:

class X;
void X::foo() { }		// Error!

Referring to namespaces

Qualified names where the base name refers to namespaces generally only require a declaration of the referred element within that namespace:

namespace N { class X; }
void foo(N::X x);		// OK

Initializer lists

The type of an initializer identifier must generally be defined within an initializer list, since the compiler must be able to call the constructor:

class X;
class Y
{
	Y();
	X x;			// That's also an error, but not the point here.
};
 
Y::Y() : x()			// Error! X must be defined here.
{ }

Note that the type of an initializer identifier must only be declared if it's a pointer or reference type:

class X;
class Y
{
	Y();
	X *x;
};
 
Y::Y() : x(0)			// OK
{ }
class X;
class Y
{
	Y(X &x);
	X &x;
};
 
Y::Y(X &x) : x(x)		// OK
{ }

Base types

Derived types require the definition of their base types:

class Base;
class Derived : Base { };	// Error!

Operator overloading

Overloading the ampersand operator of a composite type prevents that type from being safely used within a forward declaration. This is because in

class X;
X *foo(X &x) { return &x; }

the foo function returns the address of x, while in

class X { X *operator &(); };
X *foo(X &x) { return &x; }

the foo function returns the result of the overloaded operator ampersand instead. The result of the &x expression is therefore not well defined. Composite types which overload the ampersand operator must therefore always be defined.

Macros and type definitions

Both macros and type definitions always require to be defined when used.

Enumerations

The new C++11 standard made it possible to forward declare enumerations. They can now be forward declared as follows:

enum class X;			// OK – scoped, default int type
enum class X : int;		// OK – scoped, type int
enum X : int;			// OK – unscoped, type int

Note that enumerator values themselves however still always require their definition.

Templates

Class templates

If a template class only needs to be declared, all of its template arguments also only require a declaration:

template <typename T> class X;
class Y;
X<Y> *x;			// OK

If a template class needs to be defined, then it depends on the code of the template class whether a template argument also requires a declaration or only needs a definition. In the following code snippet, class Y only needs to be declared since class X<Y> only declares a pointer of type Y:

template <typename T> class X { T *t; };
class Y;
X<Y> x;				// OK

In contrast to that, class Y must be defined within the following code snippet because class X<Y> declares a non-pointer member of type Y:

template <typename T> class X { T t; };
class Y;
X<Y> x;				// Error!

So the code of the template has to be considered in order to evaluate whether the template parameter can be declared or has to be defined. This can be quite tricky because such a check has to consider template specializations and other template implementation details.

Function templates

The situation is similar when talking about function templates. In the following code snippet, class X only needs to be declared:

template <typename T> T foo(T t) { return t; }
class X;
void bar(X *x)
{
	foo(x);			// OK
}

In contrast to that, class X must be defined within the following code snippet:

template <typename T> T foo(T t) { return t + 1; }
class X;
void bar(X *x)
{
	foo(x);			// Error!
}

So it again depends on the implementation of the foo function whether we need a declaration or a definition for class X.

Type casts

Implicit type casts are already handled by the rules for the assignment operator. Explicit type casts always need the definition of the underlying type of the target type:

class X;
void foo(X &x)
{
	(X)x;			// Error!
}

Dynamic casts (and other casts as well?) always need the definition of the underlying types of both the target and the source type:

class X;
class Y;
void foo(X *x)
{
	Y *y = dynamic_cast<Y *>(x);		// Error!
}

Back to the top