www.digitalmars.com [Home] [Search] [D]

The C Preprocessor Versus D

Back when C was invented, compiler technology was primitive. Installing a text macro preprocessor onto the front end was a straightforward and easy way to add many powerful features. The increasing size & complexity of programs have illustrated that these features come with many inherent problems. D doesn't have a preprocessor; but D provides a more scalable means to solve the same problems.

Header Files

C and C++ rely heavilly on textual inclusion of header files. This frequently results in the compiler having to recompile tens of thousands of lines of code over and over again for every source file, an obvious source of slow compile times. What header files are normally used for is more appropriately done doing a symbolic, rather than textual, insertion. This is done with the import statement. Symbolic inclusion means the compiler just loads an already compiled symbol table. The needs for macro "wrappers" to prevent multiple #inclusion, funky #pragma once syntax, and incomprehensible fragile syntax for precompiled headers are simply unnecessary and irrelevant to D.

The syntax:

	#include <stdio.h>
	
is expressed in D as:
	import stdio;
	

#pragma once

Is rendered irrelevant by the import statement.

#pragma pack

This is used in C to adjust the alignment for structs. For D classes, there is no need to adjust the alignment (in fact, the compiler is free to rearrange the data fields to get the optimum layout, much as the compiler will rearrange local variables on the stack frame). For D structs that get mapped onto externally defined data structures, there is a need, and it is handled with:
	struct Foo
	{
		align (4):	// use 4 byte alignment
		...
	}
	

Macros

Preprocessor macros add powerful features and flexibility to C. But they have a downside: Here's an enumeration of the common uses for macros, and the corresponding feature in D:

  1. Macros as constants:
    	#define VALUE	5
    	
    In D:
    	const int VALUE = 5;
    	
  2. Macros to protect a .h file from multiple inclusions:
    	#ifndef STDIO_H
    	#define STDIO_H 1
    		...
    	#endif // STDIO_H
    	
    In D:
    	import stdio;
    	
  3. Macros as a list of values or flags:
    	int flags:
    	#define FLAG_X	0x1
    	#define FLAG_Y	0x2
    	#define FLAG_Z	0x4
    	...
    	flags |= FLAGS_X;
    	
    In D:
    	enum FLAGS { X = 0x1, Y = 0x2, Z = 0x4 };
    	FLAGS flags;
    	...
    	flags |= FLAGS.X;
    	
  4. Macros to distinguish between ascii chars and wchar chars:
    	#if UNICODE
    	    #define dchar	wchar_t
    	    #define TEXT(s)	L##s
    	#else
    	    #define dchar	char
    	    #define TEXT(s)	s
    	#endif
    
    	...
    	dchar h[] = TEXT("hello");
    	
    In D:
    	import dchar;		// contains appropriate typedef for dchar
    	...
    	dchar[] h = "hello";
    	
    D's optimizer will inline the function, and will do the conversion of the string constant at compile time.

  5. Macros to support legacy C compilers:
    	#if PROTOTYPES
    	#define P(p)	p
    	#else
    	#define P(p)	()
    	#endif
    	int func P((int x, int y));
    	
    D doesn't have legacy compilers, and so doesn't need legacy support. (yet!)
  6. Macros as typedefs:
    	#define INT 	int
    	
    In D:
    	typealias int INT;
    	
  7. Macros to change storage class for declaration vs definition:
    	#define EXTERN extern
    	#include "declations.h"
    	#undef EXTERN
    	#define EXTERN
    	#include "declations.h"
    	
    In declarations.h:
    	EXTERN int foo;
    	
    In D, the declaration and the definition are the same, so there is no need to muck with the storage class to generate both a declaration and a definition from the same source.
  8. Macros as lightweight inline functions:
    	#define X(i)	((i) = (i) / 3)
    	
    In D:
    	int X(int i) { return i = i / 3; }
    	
    The compiler optimizer will inline it; no efficiency is lost.
  9. Macros to pass the assert function file and line number information:
    	#define assert(e)	((e) || _assert(__LINE__, __FILE__))
    	
    In D, assert() is a built-in expression primitive. Giving the compiler such knowledge of assert() also enables the optimizer to know about things like the _assert() function never returns.
  10. Macros to change function calling conventions:
    	#ifndef _CRTAPI1
    	#define _CRTAPI1 __cdecl
    	#endif
    	#ifndef _CRTAPI2
    	#define _CRTAPI2 __cdecl
    	#endif
    
    	int _CRTAPI2 func();
    	
    D determines the optimal calling convention for you, so there is no need to override the default or to fiddle with it with macros.
  11. Macros to hide __near or __far pointer wierdness:
    	#define LPSTR	char FAR *
    	
    D doesn't support 16 bit code or mixed pointer sizes, and so the problem is just irrelevant. Of course, this problem may return with mixed 64 bit and 32 bit code.

Conditional Compilation

Conditional compilation is a powerful feature of the C preprocessor, but it has its downside: D supports conditional compilation in 4 ways:
  1. Separating version specific functionality into separate modules.
  2. The debug statement.
  3. The version statement.
  4. The if (0) statement.

Copyright (c) 1999-2001 by Digital Mars, All Rights Reserved