So, why'd I choose C++ to write Wesnoth? After all, it's a rather ugly language, and it's hideously difficult to learn.
I think the 'difficult to learn' part is pretty much a deal-breaker for anyone who doesn't already know it well before they start their project, unless part of the intent of embarking on a project is to learn C++. Personally, I already knew C++, so it wasn't a barrier.
There is another side-effect though, and that is that the pool of programmers who know C++ is alot smaller than the pool of programmers who know C, or Java, or Perl, or Python. This can be significant, especially if you're hoping to have alot of people working on your project.
Well, I wasn't really expecting anyone to help with programming on Wesnoth. I was hoping for some help with graphics certainly, but I thought I had the programming side pretty much covered -- so, this wasn't a barrier for me either. As a side note, I recommend anyone starting a project to plan on getting no help in the programming department. Substantial chunks of programming help didn't show up for a long time in Wesnoth development, and I think it's the same with most projects.
So, C++ being difficult to learn wasn't much of a barrier for me, but I do think it would be for someone who doesn't know it pretty well before they start their project. Now we have the matter of C++ being well...ugly, and often much lower-level than other languages. What advantages does it have to overcome these disadvantages?
Being an Open Source project, Wesnoth has relied heavily on people -- especially me -- being able to add a substantial chunk to the project in a small amount of time. I'm an impatient person, and I just had to be able to work for an 8, or 4, or 2, or even 1 hour chunk on Wesnoth and have something to show for it at the end. The work also had to be interesting (for me) and fun.
To start with, my development model couldn't rely heavily on unit tests. I wouldn't mind writing some unit tests for easy-to-encapsulate modules, such as the parser, but relying on unit tests for every module as a normal part of my development cycle really wouldn't work for me. It'd make development boring, and slow it down: having to write unit tests for all code I wrote would make it much more difficult to implement a feature in a one or two hour time slice.
I know lots of people swear by unit tests, and I'm not really going to go into why I don't like using them, but I will simply say that I don't, and I don't think Wesnoth would have gotten off the ground if it had to rely on unit tests. For someone who doesn't mind or likes writing unit tests, then a different development model may work better.
Anyhow, with no unit tests, I had to have a language that had lots of static checking built-in to catch common errors I would make when trying to develop fast. This quickly eliminated languages like Python and Perl. In fact, even a language like Java is more or less eliminated, since it's static checking isn't nearly as comprehensive as C or C++'s.
For instance, in C++ one can use the const keyword to promise that an object won't be modified:
- Code: Select all
void display_unit(const unit& myunit);
The 'display_unit' function shouldn't modify the unit given to it, and it indeed promises this by making the unit const. The compiler will emit an error if the function attempts to modify the unit.
Understanding how const works exactly is one of the many difficult aspects of C++ to master, but once it's second-nature the time-savings are impressive with the number of errors the compiler spots for you.
C and Objective-C are the only other major languages I know of with equivalent features to this.
Unfortunately, C has the serious disadvantage of not containing generics, and that means that it's almost impossible to implement type-safe containers in it. So for instance, in C if you had a list of units, and wanted to inspect the last one, you might have to do something like this (with a typical list implementation):
- Code: Select all
unit* u = (unit*)list_get_back(units);
This is nasty because the compiler is doing an unchecked conversion -- assuming that what is in the container is a unit. The compiler has no way to check for you that it's really a unit, and if you had some code that accidentally inserted some other type of object in the container, you would get a nasty crash.
In C++, you can make a list of unit objects, and the compiler will guarantee at compile-time that only genuine units go into it, and then you can retrieve a unit without a cast:
- Code: Select all
unit* u = units.back();
At the time I started Wesnoth, Java had the same problem as C, though later versions of Java have implemented basic generics which support this. Python, Perl, and similiar languages have the same project as C in this regard. The languages other than C do have sufficient introspection capabilities to allow one to determine at run-time the type of the object, but this isn't even close to as useful as static type checking.
Many people don't like static type checking for various reasons. However almost all of the intelligent arguments I have seen against it advocate some kind of unit testing system instead. Unfortunately, this still places the burden on the programmer, while static type checking makes the compiler easily spot many possible errors.
There is another advantage of C++ though, which I consider to possibly be its biggest advantage over any other imperative language. It's also a hugely under-rated and misunderstood advantage. That advantage is the presence of destructors.
Operations in programming often occur in pairs. You allocate memory, then later you release memory. You open a file, then later you close it. You lock a mutex, then later you unlock it. You begin an operation, then later you either commit it or roll it back.
Unfortunately, a very common programming error is to have cases where an operation is performed, but its partner operation isn't. For instance, suppose we have a function to read a file and process it in C:
- Code: Select all
int read_file(const char* fname, int fsize)
{
char* ptr = (char*)malloc(fsize);
FILE* file = fopen(fname,"r");
fsize = fread(ptr,1,fsize,file);
/*process the file here*/
fclose(file);
free(ptr);
return SUCCESS;
}
Well, there's a problem with that code. What if the file didn't open properly? So, someone spots the bug, and wants to fix it nice and fast, so they just add this below the fopen() call:
- Code: Select all
if(file == NULL) {
return ERROR;
}
But...oops...they forgot to free() the memory allocated by the malloc(). This problem is called a memory leak.
Now, of course, many languages solve this problem through garbage collection. That is, the program doesn't have to free memory allocated, the language will use some mechanism to automatically detect when it is no longer used, and reclaim it.
This is fine as far as it goes, but memory allocation and deallocation is only one of many paired operations in programming. How about opening a file and closing a file? Or opening a socket and closing a socket? Most languages with garbage collection do support operations known as 'finalizers' which are called when an object is to be destroyed, and they could close sockets and files, but they have a serious problem in that the languages make no guarantees of when they will run -- or even whether they will be run at all.
Once in a project at a company I had some code like this which I had to maintain:
- Code: Select all
void graph::draw()
{
lock_display(); //stop operations being drawn to the screen so there is no flickering
//complex drawing operations which call lots of other functions
...
unlock_display(); //update the display
}
Well, somewhere, somehow this function was occasionally exiting without unlock_display() being called. This meant that the graph was never unlocked, and would appear to the user to freeze up, permanently. It was nasty to debug, to find out when exactly this could happen.
You can't trust an operation like this to a finalizer, because the finalizer might not get called for a long time, and it might not get called at all.
Fortunately C++ has a solution to this in destructors. Destructors are guaranteed to be run when an object is destroyed, and objects created on the stack are guaranteed to be destroyed when their scope ends. So I re-wrote the above function like this:
- Code: Select all
class display_locker
{
public:
display_locker() {lock_display();}
~display_locker() {unlock_display();}
};
...
void graph::draw()
{
display_locker locker;
//complex drawing operations which call lots of other functions
...
}
This guarantees that unlock_display() will be called when the function call exits. It doesn't matter if the code runs off the end of the function, or has an early return code added, or throws an exception, or uses a longjmp statement. It's guaranteed that the destructor will be called as part of the function's exit process.
This technique is incredibly useful. It can be used to ensure that all manner of paired operations occur. I would say that Wesnoth's rather good reliability can be attributed mostly to extensive use of this C++ feature.
No other language I know of provides this feature. Not Java, or Python, or C, or Perl, or Objective-C. Some, such as Java and Python do support the 'finally' keyword, but frankly, robust error handling with such languages requires a try...finally block around almost every non-trivial function, and it still doesn't come close to providing the functionality of C++ destructors. And people call C++ ugly?
C++ has several other big advantages -- its C compatibility means that there are numerous Open Source libraries available to easily interface with, and its standard library provides a nice framework to work with. It is also available on many platforms, and thanks to its use, Wesnoth is available for lots of operating systems.
C++ is far from perfect, and I hope someday there is a better language available, but my opinion from use of C++ is that it's the best language available today to program a medium size of large game project in. (For something small, Python, Ruby, Perl, etc would probably be okay).
Finally, I don't mean to start a flame war here. It's just that lots of people have asked "why C++?" and I thought I would put my thoughts down rather than having to explain to them each time. Even if you don't agree with C++ being a good language to use, at least you might learn something by reading some of the advantages I think it has. And finally, since you're here, you probably think Wesnoth is a good game, so do consider that apparently C++ can do something right.
David
