A bit of C++ for Objective-C programmers
With the advent of Apple’s Swift, and it’s apparent success, Objective-C is quickly becoming a dying language. Good riddance, but what about all those codes that were written in Objective-C? Clearly Apple expects you to switch to Swift, and for Cocoa or iOS codes that is a sensible choice. It just so happens that I have some game codes written in Objective-C, and I’m personally not all that crazy about Swift. OK, Objective-C isn’t going away (yet), but it’s interesting to see what that code would be like when rewritten in C++.
I actually do like the Objective-C language, and I think it’s a pretty good fit for programming games. The reason for this is that it is C with classes. In my opinion, C with classes is the holy grail of programming. C++ also adds classes to C, and a whole lot more. It is a big language with many features and sometimes horrible syntax, making it a difficult language to learn. We can start off easy and build it from there.
In Objective-C you have learned to use the Foundation classes; everything is
an object, notably an NSObject
. Objective-C code typically leans heavily on
classes that are provided by frameworks. One reason for this is the reference
counting system, which can only work if every class inherits from NSObject
.
They go as far as to put integers and floats into NSNumber
objects, just
to be able to store them in an NSArray
.
For C++ and especially for game codes I like adopting a code style that is
closer to the metal, closer to C. Therefore the C++ classes that we use will
mostly be defined by ourselves, and sometimes provided by the STL (Standard
Template Library).
Let’s have a look at some Objective-C game code and convert it by example.
// Player.h (Objective-C)
#import "Monster.h"
#import "Color.h"
@interface Player : NSObject {
Monster *ship;
Color *color;
NSInteger score;
NSDate *timeOfDeath;
}
@property (retain) Monster *ship;
-(void)move;
-(BOOL)dead;
-(BOOL)collideWithObject:(WorldObject *)o;
-(void)draw;
@end
extern Player *player;
So we have our Player
class, which is basically a Monster
with its own
color and score. By the looks of it, Player
could have been a subclass
of Monster
, but I didn’t go that route here. The idea is that the Player
controls a ship
, and the ship
is so much like a Monster
, that I
chose the ship
to be a Monster
.
As always in Objective-C, the class inherits from NSObject
. We see
NSInteger
and NSDate
. Let’s stick to good old C’s int
and time_t
for the C++ version.
The ship
property is retained. C++ has no such thing, as it does not do
reference counting like Objective-C. (* It does have ‘smart pointers’, but
more on that later. For now I will stay away from those).
A rather straightforward translation of the code to C++ would be:
// Player.h (C++)
#include "Monster.h"
#include "Color.h"
class Player {
public:
Monster *ship;
const Color *color;
int score;
time_t timeOfDeath;
Player();
~Player();
void move(void);
bool dead(void) const;
bool collideWithObject(const WorldObject *o);
void draw(void) const;
};
extern Player *player;
Player()
and ~Player()
are the constructor and destructor. The constructor
is like init
, and the destructor is like dealloc
. I say “like” because the
two are not identical. They aren’t identical because C++’s inner workings are
different from Objective-C’s.
What are all those const
keywords doing? Remember that Objective-C has a
immutable objects? A const
object is almost the same, but it is more like
a promise that we will not change the object. A const
member function
promises not to change this
object (the self
, in Obj-C speak). If that
promise is broken, the compiler will error out, but it is a promise that may
be casted away if you really want to. You might even do without const
at all
as it tends to get in the way when you don’t rigorously pursue it.
I set the color
member to const
here because it wasn’t a property
in the
Objective-C interface, so it probably was just a raw pointer to a static
object.
Before we move on to the implementation, I would like to mention that we have the option of doing things quite differently, much more C-plus-plussy. However, the closer we stick to the Objective-C declaration, the fewer changes need to be made to the implementation.
// Player.m (Objective-C implementation)
#import "Player.h"
Player *player = nil;
@implementation Player
@synthesize ship;
-(id)init {
self = [super init];
if (!self)
return nil;
ship = [[Monster alloc] init];
color = [[Color alloc] initWithRGBA:0xff0000ff];
return self;
}
-(void)dealloc {
[ship release];
[color release];
[super dealloc];
}
-(void)draw {
[ship draw];
[scoreboard drawScore:score];
}
We see here that color
wasn’t just some static pointer, but apparently
it didn’t need any setters/getters so it was not a synthesized property.
The C++ code is much the same, but note that it doesn’t use super
.
In this case, we don’t use any inheritance at all. If we did, then we would
have to invoke the superclasses constructor. For the destructor this
invocation is implicit.
// Player.cpp (C++ implementation)
#include "Player.h"
Player *player = nullptr;
Player::Player() {
ship = new Monster();
color = new Color(0xff0000ff);
score = 0;
timeOfDeath = (time_t)0L;
}
Player::~Player() {
delete ship;
delete color;
}
void Player::draw(void) const {
ship->draw();
scoreboard->drawScore(score);
}
The constructor/destructor pair are so simple, they may be inlined in the class declaration in the header file.
Now, you may run into trouble because delete
is not the same as release
.
C++’s raw pointers are not reference counted, so delete
will delete the
object right there, regardless of whether the pointer was still in use. It is
up to the programmer to get this right — just as it is up to the programmer
to get the number of retain
/release
calls right — but read on.
Let’s stir things up a little. Let’s have a look at some explosions:
void init_explosions(void) {
all_explosions = [NSMutableArray arrayWithCapacity:NUM_EXPLOSIONS];
}
void reset_explosions(void) {
[all_explosions removeAllObjects];
}
void add_explosion(WorldObject *o) {
Explosion *x = [[Explosion alloc] initWithOriginal:o];
[all_explosions addObject:x];
[x release];
}
So, we have an array that holds our explosions. In C++ we could use a simple
static array, or we can use the STL vector
class. An std::vector
is a
growable array, that may be initialized to have a certain capacity.
Since our array won’t grow beyond that maximum capacity, we might also
choose an std::array
. There is more than one way to do it.
The vector
is a template class, which means that you can make vectors for
any given type. But unlike an NSMutableArray
, a vector can not hold mixed
types. [This statement is more or less false … because we are storing
pointers to Explosion
s, we can also store any subclass of
Explosion
because of polymorphism. And this is exactly the same in
Objective-C, where you are effectively storing subclasses of NSObject
].
#include <vector>
void init_explosions(void) {
all_explosions = new std::vector<Explosion *>(NUM_EXPLOSIONS);
}
void reset_explosions(void) {
// bug: memory leak here
all_explosions->clear();
}
void add_explosion(const WorldObject *o) {
Explosion *x = new Explosion(o);
all_explosions->push_back(x);
}
C++ has no retain
/release
, so we here we have a memory leak on our hands.
The clear()
method will clear the array vector, but it will not
delete the stored explosions (!)
We can fix this by calling delete
on each and every element, or we can use
a technique that is more akin to release
: by using a smart pointer.
A smart pointer knows when memory should be freed, and takes care of this
automagically.
#include <vector>
#include <memory>
void init_explosions(void) {
all_explosions = new std::vector<std::unique_ptr<Explosion> >(NUM_EXPLOSIONS);
}
void reset_explosions(void) {
all_explosions->clear();
}
void add_explosion(const WorldObject *o) {
auto x = std::make_unique<Explosion>(o);
all_explosions->push_back(std::move(x));
}
This code does not leak because the unique_ptr
object will free the memory
once the smart pointer itself is destructed. Here we use unique_ptr
,
because the object is not shared. Objective-C’s retain
acts like
shared_ptr
, which has a reference count.
A unique_ptr
does not share, so you even can not assign it to another
variable. That’s why we move
the explosion object from variable x
into
vector all_explosions
.
Personally I’m a very classic C programmer and I’m not fond of the smart
pointer syntax, so I usually stick with regular, raw pointers, at the risk
of having leaks.
The C++ language allows you to use object instances rather than pointers to instances, which is a game changer. For example, we could have written the above explosion code as:
#include <vector>
std::vector<Explosion> all_explosions(NUM_EXPLOSIONS);
void reset_explosions(void) {
all_explosions.clear();
}
void add_explosion(const WorldObject& o) {
Explosion x = Explosion(o);
all_explosions.push_back(x);
}
Hurray, no more pointers! Told ye C++ was nice.
This code has now been simplified so much, we don’t even need the vector
class anymore, and could easily do with a simple C array.
As you can see, C++ allows instances of objects to be constructed on the stack. This will implicitly invoke the constructor, and it will implicitly destruct the object when it goes out of scope (because the stack is being unwinded). Thus you use objects in a very natural way, the same as any other variable—unlike Objective-C, where every object is a pointer to the instance. In terms of performance, copying objects is more expensive than shuffling pointers around. The C++ compiler can optimize certain cases however so it may eliminate certain copy actions.
C++ is more rigid than Objective-C. For example, you can not dynamically extend a class (categories) and there are no protocols in the way that you are used to in Objective-C. While C++ is not as dynamic, in return you get higher performance.
There are lots of things I didn’t touch upon because C++ is such a versatile (read: large) language. Most notably, operator overloading. Operator overloading is ideal for matrix math, 2D and 3D vector classes. C++ is a rich language that is maybe too powerful for its own good … which is to say, you can make things really hard on yourself by getting too much caught up in unnecessary complex constructs. Keeping things simple allows you to maintain a good overview of the code. This alone can be a challenge in itself. Game programming is hard enough as it is already.