Sean: I'm sorry, it took me a while to reply because I haven't been online much in the past few days. I see what you are saying, but I still don't agree. Maybe I won't agree until I learn other languages.
(hehe BTW, the guy could have used static member variables. The only use for globals in C++ is main(), and grudgingly, at that.
)
And about optimization, you meant future developments in optimization? You did say "any lowered performance in C++ would no doubt also be seen in C..." which I took to mean "removing past optimization work," and not "limiting future optimization work" (if that makes any sense). In my original words, performance isn't going to be "lowered." However, I never said the increases wouldn't be slower.
To my knowledge, Intel assembly has types (albeit very few), and allows variable naming. Perhaps that's just another MS extension (I was looking at the MASM reference). Anyway, I suppose you could say that the size of the spec is fairly represented by the number of keywords and their context. If you separate keywords from context, you get far fewer keywords, but each keyword has multiple meanings. If we do "keywords * context = complexity" then that seems fair enough. Each assembly keyword has one context, so if there are 255 keywords the complexity is 255, right? I'd wager that C either meets or exceeds that level of complexity, and C++ has an even higher level of complexity. That is, if I had any money.
The second through fourth paragraphs of my argument about language complexity would still stand, then?
I still don't understand why you would ever want polymorphism to work with templates. That would limit the use of the templated classes and functions to derived types, and intrinsic types would require wierd workarounds like this:
class map_key_int
: public magic_class
{
public:
operator int () { return value; }
private:
int value;
};
And you couldn't even write a template class to make that easier, because you'd need the workaround to use the intrinsic types, which would require having the magic wrapper class, which would require the workaround, etc.
Not to mention all of the dynamic_cast-ing and temporary pointers the compiler _must_ do to retain type safety:
magic_value* lookup_function(map_key_int key)
{
return dynamic_cast<magic_value*>( map_object[key.value] );
}
map<magic_value_base*>* build_map(magic_value_base* objects, int number_of_objects)
{
map<magic_class*, magic_value_base*>* = new map<magic_class*, magic_value_base*>
for( int iterator=0; iterator < number_of_objects; iterator++ )
{
dynamic_cast<magic_value*>( *map[magic_class(iterator)] ).object = objects[iterator];
}
return map;
}
Ugly.
And how would it work with the lookup function? How could the map class possibly know that each map_class* is really a pointer to whatever type you thought up, with whatever key value you wanted? All the map can do with the "polymorphism" method is to use the class instances (i.e., pointers) as lookup values, which means you've got all your objects stored in random places throughout the map. You may even be overwriting them, because it depends on whether the compiler will re-use the temporary magic_class object's location or not. How could this possibly work?
And even if those things _could_ be optimized out, the messy syntax would remain. Or should we "bend" the rules a bit with templates? Allow implicit dynamic casts? etc. In short, why would you want templates to use polymorphism if you aren't even making use of polymorphism? Your method is not doing anything; it's _just_ a workaround, providing more work than is necessary, and eliminating a lot of options in the process. If you want to use polymorphism, just make it a map of pointers. Forcing all templates to use polymorphism to work would really make them useless.
Look at this piece of code, which I use all the time with DX:
template <typename pointer_type>
inline void safe_release(pointer_type& pointer)
{
if( pointer )
{
pointer->Release();
pointer = NULL;
}
}
That makes it kinda simple - anytime during shutdown that you want to release a DX object, you just call safe_release(). If you need to have redundant cleanup code somewhere, you can call it twice on the same pointer with no ill effects. I use a similar version for heap objects called safe_delete(). Templates make the safe_release() function very simple and very useful during debugging. If the class has a Release() method, then it compiles fine with no complaints, but if the class has no Release() method, a compiler error is thrown to indicate that the function won't work with it.
Do you know the all the workarounds I would have to implement to use it with the "polymorphism" method? They would completely destroy any benefit I might have gained by writing the safe_release() function. Here's a sample:
class direct_draw_object
: public magic_value_type // what does this have to do with DX objects?
{
public:
LPDIRECTDRAW p;
operator LPDIRECTDRAW()
{ return p; }
};
class direct_draw_surface_object
: public magic_value_type
{
public:
LPDIRECTDRAWSURFACE p;
operator LPDIRECTDRAWSURFACE()
{ return p; }
}
};
// etc. - how many DX object types does a DX game use?
Templates are absolutely invaluable for small utility classes and simple, type-safe but generic functions. Not to mention all sorts of container classes. And they really show the power of function overloading. If we used the templates with the "polymorphism" method, writing templates would be a worthless waste of time, because all of the time gained by encapsulating principles in generic templated code would be spent in workarounds, which happen to make the code look even less clean than void pointers (yuck!).
BTW, dynamic_cast should only be used _rarely_ - overuse of it indicates poor design and unnecessary use of polymorphism. Polymorphism is one of the most overused things in C++. You only _need_ polymorphism for a few classes in a typical game. Classes are powerful enough without polymorphism (and darn fast, too) for most chores:
class input_wrapper
{
public:
virtual void read(void*, int) = 0;
};
class output_wrapper
{
public:
virtual void write(void*, int) = 0;
};
class io_wrapper
: public input_wrapper, public output_wrapper
{
};
class file_wrapper
: public io_wrapper
{
public:
virtual void read(void*, int) { /* ... */ }
virtual void write(void*, int) { /* ... */ }
};
That example would be obvious overkill for something that simply wraps file access with some special formatting. All you really need is this:
class file
{
public:
void read(void*, int) { /* ... */ }
void write(void*, int) { /* ... */ }
};
Real, working, efficient C++ code is incredibly small.
If you are going to use abstraction properly, it is the "weeding out" of unnecessary details, and in program design it serves to eliminate unnecessary work. In a typical program, the programmer understands how files work, and the similarities and differences between file I/O and other I/O. But those relationships (like from what classes the file example was derived) are unnecessary baggage (and useless code) unless they actually _do_ something for the programmer. Form follows function, as they say. The base class concepts were totally unnecessary in the example - they accomplished nothing, except to slow the code down a small bit versus regular class functions. If you are only deriving one class, it isn't polymorphism, is it? Of course, if you were deriving another class like "network" then the virtual functions would start to become very useful, and could eliminate a lot of other code in some places. Polymorphism is a nice tool, but it shouldn't be used everywhere for everything.
That said, I think templates were one of the best additions to C++. The extra size of the language required to support templates is almost trivial, isn't it? One new keyword, two new contexts, and it adds a new methodology to the language (after procedural, structural, OOP, there comes generic). The hard part of learning templates I attribute to hype and poorly written "tutorials." After all, you aren't learning a new type of class or function per se, it's more like learning how to use existing classes and functions in a different way. In other words, using templates allows you to define classes and functions that work with any data type that supports the minimum of stated features (for example, a templated min() function would require only comparison operators). Specializing generic programming would eliminate its use.
Also, the file class (without polymorphism) could be written in this way, which shows how nice the sizeof operator really is:
class file
{
public:
template <typename data_type>
void read(data_type& data) { /* use sizeof(data) */ }
template <typename data_type>
void write(data_type& data) { /* use sizeof(data) */ }
};
(Remember, the sizeof() operator would have been used and its return value passed as a parameter to read/write anyway. Templated functions like this eliminate the chance of user error (passing the wrong type to sizeof()), because the compiler knows the type.)
Of course, you could probably go further and create templated operators. And on and on... templates really make life much easier for the user of the class. Of course, you could optimize that by: 1) making read/write inline, 2) creating a protected overloaded version that uses byte*, and 3) placing the common code from the templated functions into read and write. This eliminates the need of the user to use the sizeof() operator before every read/write operation, without even generating much work for the file class programmer. Here's the modified file class:
class file
{
public:
typedef byte unsigned char;
template <typename data_type>
inline void read(data_type& data)
{ read( reinterpret_cast<byte*>(&data), sizeof(data) ); }
template <typename data_type>
inline void write(data_type& data)
{ write( reinterpret_cast<byte*>(&data), sizeof(data) ); }
private:
void read(byte* data_pointer, int data_size)
{ /* call fread or something */ }
void write(byte* data_pointer, int data_size)
{ /* call fwrite or something */ }
};
A little bit of typing, a creative use of templates, and now the user of the file class can do this:
int x;
float y;
char z;
file.read(x);
file.read(y);
file.read(z);
Instead of this:
int x;
float y;
char z;
file.read(x, sizeof(int));
file.read(y, sizeof(float));
file.read(z, sizeof(char));
Because templates enable the compiler to juggle the types and generate the proper code. And there is no slow-down over the traditional method, because the public templated read and write functions are inline and only use the sizeof operator to call the commonly used function: read(byte*, int).
Well, I've probably bored everyone to tears by now...
Thanks for your time, Sean
quote:
Original post by MadKeithV
Ps: Null_pointer: Nobody can agree on what exactly OOP is, every book has a different definition, so really it's damned hard to talk about it . I think it generally amounts to a "data oriented approach" as opposed to an "algorithm oriented approach".
Perhaps this is why it's not so popular in Games? Most of the time technology is advancing in the algorithms, and not in the dataset or data representation...
Then what a stupid term...why in the WORLD do people have -vague- goals and -vague- methodologies? Just put organize your code so that it treats objects as more important than algorithms, but don't take it to the logical end so no one can really debate it without disagreeing? I'd rather create my own methodology. Maybe I'll start a thread about methodologies...
-
null_pointerSabre MultimediaEdited by - null_pointer on July 9, 2000 8:38:48 AM