I Like the C Preprocessor

Published November 09, 2007
Advertisement
I have a message for any of you who get the bright idea to bring up Lisp macros in response to this entry:
Shut the hell up.
Lisp macros are an entirely different game and really only work because of the way Lisp is set up in the first place. They're not relevant and certainly aren't supportable in mainstream C-based languages.

I Like the C Preprocessor

The C preprocessor gets a lot of hate. And I mean a lot of hate. Just look at Java; their reactionary horror of the whole thing was so exaggerated that they entirely forgot that conditional compilation is actually handy, and simply refused to provide anything that seemed like a preprocessor. C# was a little less blind, giving us proper conditional compilation and a few other tricks at least. I'm not talking about how much I like conditional compilation, though. Any bonehead can figure out that it's a useful thing to have. When I say I like the C preprocessor, I mean it. Textual replacement, includes, macros -- the whole deal. Are they perfect? Of course not? Do they get abused? Hell yes. Despite all that, though, I keep coming back to just how useful the damn things are. Just take a look at the heart of the internal exceptions architecture in SlimDX:
/** Copyright (c) 2007 SlimDX Group* * Permission is hereby granted, free of charge, to any person obtaining a copy* of this software and associated documentation files (the "Software"), to deal* in the Software without restriction, including without limitation the rights* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell* copies of the Software, and to permit persons to whom the Software is* furnished to do so, subject to the following conditions:* * The above copyright notice and this permission notice shall be included in* all copies or substantial portions of the Software.* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN* THE SOFTWARE.*/#pragma onceusing namespace System;using namespace System::Runtime::Serialization;#include "../Exceptions.h"#include namespace SlimDX{	namespace Direct3D9	{		[Serializable]		public ref class GraphicsException : public SlimDX::DirectXException		{		private:			static GraphicsException()			{				LastError = S_OK;				EnableForDeviceState = true;				EnableForStillDrawing = true;			}		protected:			GraphicsException(SerializationInfo^ info, StreamingContext context) : DirectXException(info, context)			{ }					public:			GraphicsException() : DirectXException(E_FAIL, "A Direct3D exception occurred.")			{ }			GraphicsException(String^ message) : DirectXException(E_FAIL, message)			{ }			GraphicsException(int errorCode ) : DirectXException( errorCode, gcnew String( DXGetErrorDescription( errorCode ) ) )			{ }			GraphicsException(int errorCode, String^ message) : DirectXException( errorCode, message )			{ }			GraphicsException(String^ message, Exception^ innerException) : DirectXException( message, innerException )			{ }			static property int LastError;			static property bool EnableForDeviceState;			static property bool EnableForStillDrawing;			static GraphicsException^ GetExceptionFromHResult( HRESULT hr );			static void CheckHResult( HRESULT hr );			static void CheckHResult( HRESULT hr, String^ dataKey, Object^ dataValue );		};#define DEFINE_GRAPHICS_EXCEPTION( ExName, ErrorCode ) \ 	[Serializable] \ 	public ref class ExName ## Exception : public GraphicsException \ 	{ \ 	protected: \ 		ExName ## Exception (SerializationInfo^ info, StreamingContext context) : GraphicsException(info, context) { }\ 	public: \ 		ExName ## Exception () : GraphicsException( ErrorCode ) { } \ 		ExName ## Exception ( String^ message ) : GraphicsException( ErrorCode, message ) { } \ 		ExName ## Exception ( String^ message, Exception^ innerException ) : GraphicsException( message, innerException ) { } \ 	}#define DEFINE_CUSTOM_GRAPHICS_EXCEPTION( ExName, ErrorCode, Message ) \ 	[Serializable] \ 	public ref class ExName ## Exception : public GraphicsException \ 	{ \ 	protected: \ 		ExName ## Exception (SerializationInfo^ info, StreamingContext context) : GraphicsException(info, context) { }\ 	public: \ 		ExName ## Exception () : GraphicsException( ErrorCode, Message ) { } \ 		ExName ## Exception ( String^ message ) : GraphicsException( ErrorCode, message ) { } \ 		ExName ## Exception ( String^ message, Exception^ innerException ) : GraphicsException( message, innerException ) { } \ 	}		DEFINE_GRAPHICS_EXCEPTION( WrongTextureFormat, D3DERR_WRONGTEXTUREFORMAT );		DEFINE_GRAPHICS_EXCEPTION( UnsupportedColorOperation, D3DERR_UNSUPPORTEDCOLOROPERATION );		DEFINE_GRAPHICS_EXCEPTION( UnsupportedColorArgument, D3DERR_UNSUPPORTEDCOLORARG );		DEFINE_GRAPHICS_EXCEPTION( UnsupportedAlphaOperation, D3DERR_UNSUPPORTEDALPHAOPERATION );		DEFINE_GRAPHICS_EXCEPTION( UnsupportedAlphaArgument, D3DERR_UNSUPPORTEDALPHAARG );		DEFINE_GRAPHICS_EXCEPTION( TooManyOperations, D3DERR_TOOMANYOPERATIONS );		DEFINE_GRAPHICS_EXCEPTION( ConflictingTextureFilter, D3DERR_CONFLICTINGTEXTUREFILTER );		DEFINE_GRAPHICS_EXCEPTION( UnsupportedFactorValue, D3DERR_UNSUPPORTEDFACTORVALUE );		DEFINE_GRAPHICS_EXCEPTION( ConflictingTexturePalette, D3DERR_CONFLICTINGTEXTUREPALETTE );		DEFINE_GRAPHICS_EXCEPTION( DriverInternalError, D3DERR_DRIVERINTERNALERROR );		DEFINE_GRAPHICS_EXCEPTION( NotFound, D3DERR_NOTFOUND );		DEFINE_GRAPHICS_EXCEPTION( MoreData, D3DERR_MOREDATA );		DEFINE_GRAPHICS_EXCEPTION( DeviceLost, D3DERR_DEVICELOST );		DEFINE_GRAPHICS_EXCEPTION( DeviceNotReset, D3DERR_DEVICENOTRESET );		DEFINE_GRAPHICS_EXCEPTION( NotAvailable, D3DERR_NOTAVAILABLE );		DEFINE_GRAPHICS_EXCEPTION( OutOfVideoMemory, D3DERR_OUTOFVIDEOMEMORY );		DEFINE_GRAPHICS_EXCEPTION( InvalidDevice, D3DERR_INVALIDDEVICE );		DEFINE_GRAPHICS_EXCEPTION( InvalidCall, D3DERR_INVALIDCALL );		DEFINE_GRAPHICS_EXCEPTION( DriverInvalidCall, D3DERR_DRIVERINVALIDCALL );		DEFINE_GRAPHICS_EXCEPTION( WasStillDrawing, D3DERR_WASSTILLDRAWING );		DEFINE_GRAPHICS_EXCEPTION( CannotModifyIndexBuffer, D3DXERR_CANNOTMODIFYINDEXBUFFER );		DEFINE_GRAPHICS_EXCEPTION( InvalidMesh, D3DXERR_INVALIDMESH );		DEFINE_GRAPHICS_EXCEPTION( CannotAttributeSort, D3DXERR_CANNOTATTRSORT );		DEFINE_GRAPHICS_EXCEPTION( SkinningNotSupported, D3DXERR_SKINNINGNOTSUPPORTED );		DEFINE_GRAPHICS_EXCEPTION( TooManyInfluences, D3DXERR_TOOMANYINFLUENCES );		DEFINE_GRAPHICS_EXCEPTION( InvalidData, D3DXERR_INVALIDDATA );		DEFINE_GRAPHICS_EXCEPTION( LoadedMeshHasNoData, D3DXERR_LOADEDMESHASNODATA );		DEFINE_GRAPHICS_EXCEPTION( DuplicateNamedFragment, D3DXERR_DUPLICATENAMEDFRAGMENT );		DEFINE_GRAPHICS_EXCEPTION( CannotRemoveLastItem, D3DXERR_CANNOTREMOVELASTITEM );		DEFINE_GRAPHICS_EXCEPTION( BadObject, D3DXFERR_BADOBJECT );		DEFINE_GRAPHICS_EXCEPTION( BadValue, D3DXFERR_BADVALUE );		DEFINE_GRAPHICS_EXCEPTION( BadType, D3DXFERR_BADTYPE );		DEFINE_GRAPHICS_EXCEPTION( XNotFound, D3DXFERR_NOTFOUND );		DEFINE_GRAPHICS_EXCEPTION( NotDoneYet, D3DXFERR_NOTDONEYET );		DEFINE_GRAPHICS_EXCEPTION( FileNotFound, D3DXFERR_FILENOTFOUND );		DEFINE_GRAPHICS_EXCEPTION( ResourceNotFound, D3DXFERR_RESOURCENOTFOUND );		DEFINE_GRAPHICS_EXCEPTION( BadResource, D3DXFERR_BADRESOURCE );		DEFINE_GRAPHICS_EXCEPTION( BadFileType, D3DXFERR_BADFILETYPE );		DEFINE_GRAPHICS_EXCEPTION( BadFileVersion, D3DXFERR_BADFILEVERSION );		DEFINE_GRAPHICS_EXCEPTION( BadFileFloatSize, D3DXFERR_BADFILEFLOATSIZE );		DEFINE_GRAPHICS_EXCEPTION( BadFile, D3DXFERR_BADFILE );		DEFINE_GRAPHICS_EXCEPTION( ParseError, D3DXFERR_PARSEERROR );		DEFINE_GRAPHICS_EXCEPTION( BadArraySize, D3DXFERR_BADARRAYSIZE );		DEFINE_GRAPHICS_EXCEPTION( BadDataReference, D3DXFERR_BADDATAREFERENCE );		DEFINE_GRAPHICS_EXCEPTION( NoMoreObjects, D3DXFERR_NOMOREOBJECTS );		DEFINE_GRAPHICS_EXCEPTION( NoMoreData, D3DXFERR_NOMOREDATA );		DEFINE_GRAPHICS_EXCEPTION( BadCacheFile, D3DXFERR_BADCACHEFILE );		DEFINE_CUSTOM_GRAPHICS_EXCEPTION( OutOfMemory, E_OUTOFMEMORY, "Out of memory." );		DEFINE_CUSTOM_GRAPHICS_EXCEPTION( Direct3D9NotFound, E_FAIL, "Direct3D 9 not found." );		DEFINE_CUSTOM_GRAPHICS_EXCEPTION( Direct3DX9NotFound, E_FAIL, "Direct3DX 9 not found." );		DEFINE_CUSTOM_GRAPHICS_EXCEPTION( Direct3DNotInitialized, E_FAIL, "Direct3D not initialized." );		inline GraphicsException^ GraphicsException::GetExceptionFromHResult( HRESULT hr )		{			GraphicsException^ ex;#			define GENERATE_EXCEPTION(errCase, type) \ 			case errCase:\ 				ex = gcnew type ## Exception ();\ 				break;#			define GENERATE_EXCEPTION_IF(errCase, type, condition) \ 			case errCase:\ 				if(condition)\ 					ex = gcnew type ## Exception ();\ 				else\ 					return nullptr;\ 				break;			switch( hr )			{			GENERATE_EXCEPTION_IF(D3DERR_DEVICELOST, DeviceLost, GraphicsException::EnableForDeviceState);			GENERATE_EXCEPTION_IF(D3DERR_DEVICENOTRESET, DeviceNotReset, GraphicsException::EnableForDeviceState);			GENERATE_EXCEPTION_IF(D3DERR_WASSTILLDRAWING, WasStillDrawing, GraphicsException::EnableForStillDrawing);			GENERATE_EXCEPTION(D3DERR_WRONGTEXTUREFORMAT, WrongTextureFormat);			GENERATE_EXCEPTION(D3DERR_UNSUPPORTEDCOLOROPERATION, UnsupportedColorOperation);			GENERATE_EXCEPTION(D3DERR_UNSUPPORTEDCOLORARG, UnsupportedColorArgument);			GENERATE_EXCEPTION(D3DERR_UNSUPPORTEDALPHAOPERATION, UnsupportedAlphaOperation);			GENERATE_EXCEPTION(D3DERR_UNSUPPORTEDALPHAARG, UnsupportedAlphaArgument);			GENERATE_EXCEPTION(D3DERR_TOOMANYOPERATIONS, TooManyOperations);			GENERATE_EXCEPTION(D3DERR_CONFLICTINGTEXTUREFILTER, ConflictingTextureFilter);			GENERATE_EXCEPTION(D3DERR_UNSUPPORTEDFACTORVALUE, UnsupportedFactorValue);			GENERATE_EXCEPTION(D3DERR_CONFLICTINGTEXTUREPALETTE, ConflictingTexturePalette);			GENERATE_EXCEPTION(D3DERR_DRIVERINTERNALERROR, DriverInternalError);			GENERATE_EXCEPTION(D3DERR_NOTFOUND, NotFound);			GENERATE_EXCEPTION(D3DERR_MOREDATA, MoreData);			GENERATE_EXCEPTION(D3DERR_NOTAVAILABLE, NotAvailable);			GENERATE_EXCEPTION(D3DERR_OUTOFVIDEOMEMORY,OutOfVideoMemory);			GENERATE_EXCEPTION(D3DERR_INVALIDDEVICE,InvalidDevice);			GENERATE_EXCEPTION(D3DERR_INVALIDCALL,InvalidCall);			GENERATE_EXCEPTION(D3DERR_DRIVERINVALIDCALL,DriverInvalidCall);			GENERATE_EXCEPTION(D3DXERR_CANNOTMODIFYINDEXBUFFER, CannotModifyIndexBuffer);			GENERATE_EXCEPTION(D3DXERR_INVALIDMESH, InvalidMesh);			GENERATE_EXCEPTION(D3DXERR_CANNOTATTRSORT, CannotAttributeSort);			GENERATE_EXCEPTION(D3DXERR_SKINNINGNOTSUPPORTED, SkinningNotSupported);			GENERATE_EXCEPTION(D3DXERR_TOOMANYINFLUENCES, TooManyInfluences);			GENERATE_EXCEPTION(D3DXERR_INVALIDDATA, InvalidData);			GENERATE_EXCEPTION(D3DXERR_LOADEDMESHASNODATA, LoadedMeshHasNoData);			GENERATE_EXCEPTION(D3DXERR_DUPLICATENAMEDFRAGMENT, DuplicateNamedFragment);			GENERATE_EXCEPTION(D3DXERR_CANNOTREMOVELASTITEM, CannotRemoveLastItem);			GENERATE_EXCEPTION(D3DXFERR_BADOBJECT, BadObject);			GENERATE_EXCEPTION(D3DXFERR_BADVALUE, BadValue);			GENERATE_EXCEPTION(D3DXFERR_BADTYPE, BadType);			GENERATE_EXCEPTION(D3DXFERR_NOTFOUND, NotFound);			GENERATE_EXCEPTION(D3DXFERR_NOTDONEYET, NotDoneYet);			GENERATE_EXCEPTION(D3DXFERR_FILENOTFOUND, FileNotFound);			GENERATE_EXCEPTION(D3DXFERR_RESOURCENOTFOUND, ResourceNotFound);			GENERATE_EXCEPTION(D3DXFERR_BADRESOURCE, BadResource);			GENERATE_EXCEPTION(D3DXFERR_BADFILETYPE, BadFileType);			GENERATE_EXCEPTION(D3DXFERR_BADFILEVERSION, BadFileVersion);			GENERATE_EXCEPTION(D3DXFERR_BADFILEFLOATSIZE, BadFileFloatSize);			GENERATE_EXCEPTION(D3DXFERR_BADFILE, BadFile);			GENERATE_EXCEPTION(D3DXFERR_PARSEERROR, ParseError);			GENERATE_EXCEPTION(D3DXFERR_BADARRAYSIZE, BadArraySize);			GENERATE_EXCEPTION(D3DXFERR_BADDATAREFERENCE, BadDataReference);			GENERATE_EXCEPTION(D3DXFERR_NOMOREOBJECTS, NoMoreObjects);			GENERATE_EXCEPTION(D3DXFERR_NOMOREDATA, NoMoreData);			GENERATE_EXCEPTION(D3DXFERR_BADCACHEFILE, BadCacheFile);			GENERATE_EXCEPTION(E_OUTOFMEMORY, OutOfMemory);			default:				ex = gcnew GraphicsException( "A graphics exception occurred." );			}			ex->HResult = hr;			return ex;		}		inline void GraphicsException::CheckHResult( HRESULT hr, String^ dataKey, Object^ dataValue )		{			GraphicsException::LastError = hr;			if( DirectXException::EnableExceptions && FAILED(hr) )			{				GraphicsException^ ex = GraphicsException::GetExceptionFromHResult( (hr) );				//don't throw if an exception wasn't returned for some reason (e.g. it's part of a disabled subset)				if( ex != nullptr )				{					if( dataKey != nullptr )						ex->Data->Add( dataKey, dataValue );					throw ex;				}			}		}		inline void GraphicsException::CheckHResult( HRESULT hr )		{			GraphicsException::CheckHResult( hr, nullptr, nullptr );		}	}}
I imagine a few of you have choked on whatever drink you were in the middle of consuming at this point. But think about it. How would you have done it? Would you really type out the exception classes one by one? No, you'd copy paste them and edit. If there were any mistakes in the snip that you copy pasted, you've now multiplied the same bug fifty times and get to fix it fifty times. And let's not forget how much scrolling you'll be doing over nearly identical blocks of code. Maintaining the SlimDX exception setup is so easy, and it's so very easy to look at it when you need to examine behavior or fix something. Not only that, macros are really easy to work with as a group using regex find and replace, because they're simple single line constructs. Trying to replace entire classes is much trickier to actually do.

Some time back, just after I wrote about in code profilers, I was working on building one in C#. Partway through, I suddenly realized that C# doesn't have line, file, or function macros. If you want that sort of information, you have to get a StackFrame object that will tell you all the details. That's all great until you remember that you're now allocating a new object every time you hit a profiled function. (Oh, and StackFrame won't work when symbols aren't available.) Allocation in .NET is cheap, but it isn't that cheap. My solution was basically to stick to coarse profiling only, which isn't too bad and doesn't really hurt the original goals that much. That's not the point. The C preprocessor provides you with an extraordinarily elegant method for embedding information available at compile time into the executable itself and doing all sorts of useful debugging tricks with it. Asserts, in code profilers, debugging with automatic file/line tagging (with no cost other than code size!), even variables with magically unique names to pull off all sorts of useful tricks.

My point is that, easily abused or not, the C preprocessor is an unbelievably useful facility. I maintain that the preprocessor is one of the places where K&R actually did something right, but a bunch of idiots screwed it up for everyone. If it came to designing a programming language, I would include a preprocessor as part of the core. In fact, I might just drop the C preprocessor in outright, without even bothering to make it smarter. Half the brilliance of that thing is that it's such a stupidly crude system, lacking basically anything more clever than macros with arguments. Now naturally I wouldn't make it such a fundamental part of the compilation system, like in C, where it's totally impossible to get along with it. Imagine something much closer to C#, but with a full text based preprocessor sitting there for when you need it. Thus it's not critical to anything, and you can skip using it completely when you don't want it.

Of course, the problem solvers in the crowd have already realized that finding a standalone C preprocessor is extremely easy and can be dropped into a build as a pre-compile step quite easily. You can even be clever about it and write a csc replacement or a custom make system that automates it. Well, that'll work. There's some subtleties involved (timestamps, dependency analysis, etc) but it will do the trick. At least it will until you attempt to share your code with anyone else, since now you have custom bits in the build system. And it will never settle in as a core idiom of the language. (Maybe that's a good thing?) Personally, I'm very, very close to actually doing it and simply making the preprocessor binary part of any code distribution or source repository that I put out. It'll be quite easy in MSBuild, although you'll have to inject an extra task and item group in between the files and the compiler by hand. Once you've done that, it shouldn't interfere with anything though. I'm still a little concerned about what will happen to file timestamps in the process, but even if a full rebuild is forced it'll probably be worth it. It probably won't get me an insane engineer award, but it's at least a point in my favor.
0 likes 3 comments

Comments

Driv3MeFar
Just FYI, this made programming.reddit.com
November 09, 2007 05:14 PM
ApochPiQ
We use macros heavily in our codebase to minimize redundant code, exactly as your example demonstrates. It's an excellent way to observe DRY for those cases where the best solution really is fifty copies of the same code, with small tweaks.
November 09, 2007 09:28 PM
JTippetts
I love the preprocessor mostly for the scattered bits of humor it has injected into my life. I still vaguely remember Washu posting (and critiquing in his typical polite fashion) some sort of horribly evil construction FORXYLOOP or something like that, where the fellow encapsulated two nested for loop opening statements inside a macro for iterating in 2 dimensions. It was hilarious. I actually cringed, and my programming style most closely resembles 'fast, loose and ugly' rather than 'clean and proper'.
November 10, 2007 08:44 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement