- Overview
-
Table of Contents
- Special Member Functions: Constructors, Destructors, and the Assignment Operator
- Operator Overloading
- Memory Management
- Templates
- Namespaces
- Time and Date Library
- Streams
- Object-Oriented Programming and Design Principles
- The Standard Template Library (STL) and Generic Programming
- Exception Handling
- Runtime Type Information (RTTI)
- Signal Processing
- Creating Persistent Objects
- Bit Fields
- New Cast Operators
- Environment Variables
- Variadic Functions
- Pointers to Functions
- Function Objects
- Pointers to Members
- Lock Files
- Design Patterns
- Dynamic Linking
-
Tips and Techniques
- Using the Swap() Algorithm
- Using class stopwatch for Performance Measurements
- Extending <tt><iostream></tt> to Support User-Defined Types
- Using <tt>auto_ptr</tt> To Automate Memory Management
- Using <tt>auto_ptr</tt> To Automate Memory Management, Part II
- Using <tt>auto_ptr</tt> To Automate Memory Management, Part III
- Using <tt>enum</tt>s as Mnemonic Indexes
- Create Objects on Pre-Allocated Memory Using <tt>Placement-new</tt>
- Online Books: <tt>Placement-new</tt>
- Bitwise Operators
- Bitwise Operators II
- Who's <tt>this</tt>?
- A Reference Guide
- The Virtues of Multiple Inheritance
- Interfaces
- Multiple Inheritance: Construction and Destruction Order
- nothrow new
- POD Initialization
- Object Initialization
- <tt>const</tt> Declarations
- The Semantics of <tt>volatile</tt>
- <tt>inline</tt> Functions
- Project Organization Guidelines
- All About <tt>bool</tt>
- <tt>typedef</tt> Declarations
- State of the <tt>union</tt>
- Dynamic Cast Uses
- Integrating C and C++
- <tt>const</tt>-Correctness
- <tt>const</tt>-Correctness: Advanced Issues
- Sprucing Up Legacy Code
- Virtual Constructors
- Naming Names
- Function Calls
- Speaking Standardese (updated)
- Speaking Standardese: the One Definition Rule
- Declarations and Definitions
- More on Declarations and Definitions
- The Most Vexing Parse
- Finally, At Last
- Sound Bytes (Admittedly Off Topic)
- Local Classes
- Complex Arithmetic
- Floating Point Woes
- String Manipulation
- The Object Model
- The Object Model II
- The Object Model III
- Temporary Objects
- Temporary Objects: Advanced Techniques
- Over-Engineering
- Security Enhancements
- Drop the (automatic) Pilot
- Choosing the Right Container
- Choosing the Right Container II
- Choosing the Right Container, Part III
- Arrays and Pointers
- Low-Level File I/O
- Low-Level File I/O Part II
- static Declarations, Part I
- static Declarations, Part II
- <code>static</code> Initialization Order
- Revisiting the Deprecation of File-Scope Static
- Virtual Memory and Memory Mapping
- Cellular Phone Programming Guidelines
- The Handle/Body Idiom
- Whole Program Optimization, Part I
- Whole Program Optimization, Part II
- Manipulating Directories
- Window Dressing
- <code>friend</code> Declarations
- <code>friend</code> Part II: the Interaction of Friendship and Template Classes
- Forcing Object Allocation on Specific Storage Types
- Lazy Evaluation
- Cache and Carry
- Controlling a Container’s Capacity
- Non-Blocking I/O, Part I
- Non-Blocking I/O, Part II
- Using Unions for Automatic Conversion
- Launching a Child Process
- <tt>switch</tt> Statements
- Introducing the "struct Hack"
- Scoped Enumerators
- Doing Statistics with STL
- Fixing the "Unresolved External" Linkage Error
- Understanding Calling Conventions
- Understanding the Empty Base Optimization
- Implementing RPC with the door Library, Part 1
- Implementing RPC with the door Library, Part 2
- Eliminating Two Common Pointer and <tt>sizeof</tt> Bugs
- Command Line Arguments
- Performance Myths Busting
- Tag Names And Types Part I
- Tag Names And Types Part II
- The Infamous goto
- Trimming Strings
- Can Objects Live Forever? Part I
- Can Objects Live Forever? Part II
- Five Ways to Improve Your Functions
- Member Aggregate Initialization
- Five Futile Coding-Style Debates
- The Good Parasite Idiom: An Exercise in OOD
- The Good Parasite Idiom: An Exercise in OOD, Part II
- The Good Parasite Idiom: An Exercise in OOD, Part III
- Ten Techniques to Reduce the Size of Your Classes, Part I
- Ten Techniques to Reduce the Size of Your Classes, Part II: Inheritance Issues
- Ten Techniques to Reduce the Size of Your Classes, Part III
- Ten Techniques to Reduce the Size of Your Classes IV
- Taking the Address of an Object with an Overloaded Operator <tt>&</tt>
- strcpy() -- How and Why Does It "Just Work"?
- Anonymous Structs
- Five Easy Ways to Reduce The Size of your Executables
- Standard Layout Classes and Trivially Copyable Types, Part I
- Standard Layout Classes and Trivially Copyable Types, Part II
- Five Simple Code Sanity Checks
- Five Things You Need to Know About C++11 Unions
- A Tour of C99
- A Tour of C1X
- C++0X: The New Face of Standard C++
- C++0x Concurrency
- The Reflecting Circle
- We Have Mail
- The Soapbox
- Numeric Types and Arithmetic
- Careers
- Locales and Internationalization
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(¶m)};
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.
