Sign in to follow this  
roos

Interesting use of #include

Recommended Posts

roos    217
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

Share this post


Link to post
Share on other sites
Sneftel    1788
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.

Share this post


Link to post
Share on other sites
njpaul    367
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.

Share this post


Link to post
Share on other sites
pragma Fury    343
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]

Share this post


Link to post
Share on other sites
MaulingMonkey    1730
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 ****....

Share this post


Link to post
Share on other sites
iMalc    2466
Nice MM, my thoughts exactly.
switch statements are an unnecesary evil.

[razz]Lookup table pwns switch in 99% of cases.[razz]

Share this post


Link to post
Share on other sites
roos    217
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 1
PARAM_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

Share this post


Link to post
Share on other sites
Gorax    202
Quote:
Original post by iMalc
Nice MM, my thoughts exactly.
switch statements are an unnecesary evil.

[razz]Lookup table pwns switch in 99% of cases.[razz]


In this case, it wouldn't help unless you had the table referencing functions which returned the new animal, so unless you wanted to create a new function for every animal, you'd stick to the switch statement.

I personally like the way they used the include file to generate those things (even if it is a little harder to follow).

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this