Interesting use of #include

Started by
9 comments, last by Gorax 18 years, 10 months ago
Hi, Check out this coding tip I found on kamron.net:
Quote:Okay, everyone knows about using includes in our .cpp or in our .h in order to call class/functions definitions. Basically includes are nothing but including text during the compilation process. Here is an interesting way to use includes: Imagine that you read a file (same problem for network), a number (id) tells you which class to create and what to fill. Here is how it is normally done in general: // the DOG and CAT classes are derived from ANIMAL while (not end of file) { ANIMAL *animal = NULL; // read first byte (id) switch (byteread) { case ENUM_CAT: // a cat { animal = new CAT; } break; case ENUM_DOG: // a dog { animal = new DOG; } break; // ......etc } animal->ReadData(); // read all data needed (using a virtual function defined in ANIMAL) } Well, everything is fine but you may have this switch in many parts of your program that are doing different things. You may have more than cats and dogs, like 50, 60, 70 more species... or even 20. It will quickly become a mess to maintain. Okay, I thing you get the picture. Here is a way to be smart and beautiful :) with a macro and an include: #define MACRO(x) case ENUM_ANIMAL_##x: animal = new x break; while (not end of file) { ANIMAL *animal = NULL; // read first byte switch (byteread) { #include "mytypes.h" } animal->ReadData(); } #undef MACRO Also, you need to create a file called mytypes.h like this: MACRO(CAT) MACRO(DOG) ... Now you only have this file (mytypes.h) to maintain! Every time you add a news class, you don't need to find where in code it should be changed, all you have to do is to add a new line in your mytypes.h. Using this technique, we create enums in a generic way in another .h like this: #define MACRO(x) ENUM_ANIMAL_##x, enum ENUM_ANIMAL { #include "mytype.h" ENUM_ANIMAL_LAST, }; #undef TEMPLATE that will generate all enums for us. - How does it work? the #define enable us to define a macro. In our example, the macro is called MACRO. When the compiler will read our file (thanks to our #include), it will simply replace all our MACRO word with what we asked for. The #include will copy the content of the included file instead of the #include"mytype.h". Because the file we include contains the word MACRO, it will be replaced with what we want (in our example a switch or an enum statement). It's exactly like we wrote it, but the compiler is doing it for us (no mistakes and everything is up to date) and we only need to maintain one file (the included file). Our code is smaller and clearer. The #undef is also important because it tells the compiler to stop changing the macro word with what we asked for. So we can create another macro using the same word further in the code
Anyways... I like this kind of thing, but what do you guys think about it? I think, if I tried to pull something like this at an actual company, people might think this is a really "hacky" way of doing things. (I've noticed a lot of coders in general seem to be extremely wary of preprocessor tricks) Thanks, roos
Advertisement
This is similar to things I have done in the past. It's basically a roundabout way of trying to support lambdas in C++. The problem is, preprocessor tricks like this tend to be very difficult to read and to debug, since they generate a lot of text that doesn't have a good line number. I'm not saying don't use it; just don't use it if you can possibly think of a better way.
Quote:Original post by Sneftel
don't use it if you can possibly think of a better way.


I could think of one but I am sure it would get all of the C++ fanatics enraged.

I think its a sloppy way of programming and makes it hard to see what is going on. In this case, I would make a function that returns a pointer to an animal and takes in the byteread. Just encapsulate the switch in the function and return the pointer. Something like this:

Animal* CreateAnimal(int byteread){    switch(byteread)    {    case: ENUM_CAT:        {            return new CAT();            break;        }    ...    }}


This doesn't solve the problem when you have to do different things though, but that should be a copy and paste. In general, if you design ahead of time, you won't run into the problem of needing to add a class that you may have forgotten. It is times like that when I wish C++ had first class functions, like Scheme does.
Quote:Original post by The Reindeer Effect
Quote:Original post by Sneftel
don't use it if you can possibly think of a better way.


I could think of one but I am sure it would get all of the C++ fanatics enraged.


I wanna hear it [smile] pm it to me... unless it involves a language other than C++, in which case I might get enraged [smile]
I'd be interested in it too. Just post it in here. I promise I won't become enraged.
It's not a particularly elegant solution, but it works - and I wouldn't be opposed to it for over-the-network stuff. I'd wrap it all up in a nice API though. Here's what I'd probably use off first bat for a network syncronization:

typedef animal (*animal_factory_function_t)( void );template < typename base_type , typename implementation_type >base_type * factory( void ) {    return new implementation_type;}animal_factory_function_t animal_factory[] =    { & factory< animal , cow >    , & factory< animal , horse >    , & factory< animal , cat >    , & factory< animal , crow >    , & factory< animal , sheep >    };assert( (byteread >= 0) && (byteread < size_of( animal_factory )) );//note: NOT the operator sizeof, rather industry::size_of, which will return the number of elements for both containers and C-style arrays.animal * animal = animal_factory[ byteread ]();
(untested)

Although I'd probably wrap *_factory[] into a class with an API as follows:

animal_factory.create( byteread )

Which would of course have the error checking and the like. This would then allow me to easly switch to the ENUMs method if I found the compiler could create better code with it. Macros could be used to shorten each line, or you could play around with classes, static functions and typedefs.

If one is doing a load/save style operation, I'd use Boost.Serialization.

Edit: Bloody hell, go in there you assert and related comments!!! Go go go!!! My internet is such ****....
Nice MM, my thoughts exactly.
switch statements are an unnecesary evil.

[razz]Lookup table pwns switch in 99% of cases.[razz]
"In order to understand recursion, you must first understand recursion."
My website dedicated to sorting algorithms
Cool, yeah that makes sense Sneftel... only save something like this for when it's really needed. Also it'd probably have to be made part of the company's coding guidelines because if everyone else is handling say, serialization one way, and you're making up crazy stuff on your own, you gonna get fired :) (even if your way is better!)

I like your idea MaulingMonkey, that'd definitely work although it does take a bit of work to figure out what it's doing for the person who's maintaining the code...

Thanks also for the link to the boost serialization stuff... I'm definitely curious about that. At my previous company, the I/O code was extremely tedious. To save or load a file, every parameter had to be separately fread'd or fwrite'd... Which isn't all that horrible by itself, but we had one of those functions for every version of every file format all the way down to 1.0 (this is of course so the world editor won't choke if you give it an old file or something like that).

So, I made a crazy system to solve this which would let you define a file format by an array of ID's which describe its structure, which allowed you to easily create new file formats as an extension of the old one, e.g.:

PARAM_ID MagicSpell_100[] ={    PARAM_HEADER1,          // parameters- can represent a variable, a structure,    PARAM_ATTACK_DAMAGE1,   // or a cluster of different related things    PARAM_MP_USAGE1,    };// create a new format which has everything the v100 did, plus 1PARAM_ID MagicSpell_101[] ={    COPY_STRUCTURE( MagicSpell_100 ),    PARAM_SOME_EXTRA_PARAMETER1};



The loading function would step through all these parameters, do a switch on the PARAM_ID, and do read/write accordingly. At the time I was pretty happy with this system because it hugely reduced the amount of code that had to be duplicated and made I/O a lot more organized. But this is probably even worse than that tip about #includes in terms of preprocessor abuse and unmaintainable code, plus it only really has a benefit for files which are likely to go through more than 2-3 versions..

roos

You should have a look at the Boost Preprocessor library. They use that kind of things extensively to pull off some really cool tricks.
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan

This topic is closed to new replies.

Advertisement