Skip to content

Instantly share code, notes, and snippets.

@SeijiEmery
Created November 14, 2014 03:23
Show Gist options
  • Save SeijiEmery/adedfaa37dd67aead5a6 to your computer and use it in GitHub Desktop.
Save SeijiEmery/adedfaa37dd67aead5a6 to your computer and use it in GitHub Desktop.
On polymorphism...
// Kind of a boring example, but suppose you're writing a vector graphics renderer and need to
// both store and render several types of shapes including circles, rectangles, and polygons
// made up of multiple lines. These all have different type signatures (a circle is not a box),
// so to store them as one type you'll have to use polymorphism.
// There's two ways to do this in C++: via OOP, or using the older, procedural C-style.
// The object-oriented approach:
class Shape : public MyObject {
// Contains shared data like the tansforms that applies to all shape types
public:
Shape (const Vec2 & pos, float angle)
: position(pos), angle(angle) {}
virtual ~Shape () {} // Obligatory if we're using virtual inheiritance
void move (const Vec2 & rel) { position += rel; }
void rotate (float degrees) { angle += degrees; }
Vec2 getPosition () const { return position; }
float getRotation () const { return angle; }
virtual void scale (float value) {}
virtual void render (Renderer & renderer) {}
// ...
protected:
Vec2 position;
float angle;
};
class Circle : public Shape {
public:
Circle (const Vec2 & pos, float angle, float radius)
: Shape(pos, angle), radius(radius) {}
void scale (float value) override { radius *= value; }
void render (Renderer & renderer) override {
// Render circle...
}
protected:
float radius;
};
class Rectangle : public Shape {
public:
Rectangle (const Vec2 & pos, float angle, float width, float height)
: Shape(pos, angle), width(width), height(height) {}
void scale (float value) override { width *= value; height *= value; }
void render (Renderer & renderer) override {
// Render box...
}
protected:
float width, height;
};
class Polygon : public Shape {
public:
Polygon (const Vec2 & pos, float angle, const std::initializer_list<Vec2> & points)
: Shape(pos, angle), points(points) {}
void scale (float value) override {
// scale points...
}
void render (Renderer & renderer) override {
// Render polygon...
}
protected:
std::vector<Vec2> points;
};
// Thanks to polymorphism, you can store multiple shape types in a single container
void example () {
std::vector<std::shared_ptr<Shape>> shapes;
// Really ugly xD (but you'd probably be using a factory method to create these, which would remove most of the uglyness...)
shapes.push_back(static_ptr_cast<Shape>(make_shared<Circle>({ 42, 12 }, 0.0f, 10)));
shapes.push_back(static_ptr_cast<Shape>(make_shared<Rectangle>({ 12, 15 }, 15.0f, 20, 40)));
shapes.push_back(static_ptr_cast<Shape>(make_shared<Polygon>({ 0, 0 }, 0.0f, {
{ 1, 1 },
{ 2, 5 },
{ 3, 7 },
{ 17, 13 }
})));
Renderer renderer { /* ... */ };
// Draw shapes
for (auto shape : shapes)
shape->render(renderer);
// Move by 50, 50 px and redraw
for (auto shape : shapes) {
shape->move({ 50, 50 });
shape->render(renderer);
}
}
// Potential ugliness solution (via more ugliness :P):
template <typename T, typename... Args>
std::shared_ptr<Shape> makeShape (Args... params) {
return static_ptr_cast<Shape>(make_shared<T>(std::forward<Args...>(params)...));
}
// Note: this makes no guarantees about the type of T, so trying to do makeShape<Foo>
// is invalid and will net you a nasty compiler error involving lots of templates.
void example2 () {
auto circle = makeShape<Circle> ({ 42, 12 }, 0.0f, 10 );
auto box = makeShape<Rectangle>({ 12, 15}, 15.0f, 20, 40);
auto polygon = makeShape<Polygon> ({0, 0}, 0.0f, {
{ 1, 1 },
{ 2, 5 },
{ 3, 7 },
{ 17, 13 }
});
std::vector<std::shared_ptr<Shape>> shapes { circle, box, polygon };
// ...
}
// And if the template part *really* annoys you, you could even do this:
#define makeCircle makeShape<Circle>
#define makeRectangle makeShape<Rectangle>
#define makePolygon makeShape<Polygon>
// OTOH, you can handle polymorphism w/out using OOP at all:
struct Shape {
Vec2 pos;
float angle;
enum class Type {
Circle,
Rectangle,
Polygon
} type;
union {
struct Circle {
float radius;
} circle_data;
struct Rectangle {
float width, height;
} rect_data;
struct Polygon {
std::vector<Vec2> points;
} poly_data;
};
};
void render (const Renderer & renderer, const Shape & shape) {
switch (shape.type) {
case Shape::Type::Circle:
// Render circle...
break;
case Shape::Type::Rectangle:
// Render rect...
break;
case Shape::Type::Polygon:
// Render poly...
break;
// No default, so the compiler should warn you if you add a new shape type w/out providing rendering code
}
}
Shape & move (Shape & shape, const Vec2 & rel) {
return shape.pos += rel, shape;
}
Shape & rotate (Shape & shape, float angle) {
return shape.angle += angle, shape;
}
Shape & scale (Shape & shape, float value) {
switch (shape.type) {
case Shape::Type::Circle:
shape.circle_data.radius *= value;
break;
case Shape::Type::Rectangle:
shape.rect_data.width *= value;
shape.rect_data.height *= value;
break;
case Shape::Type::Polygon:
// Resize polygon...
break;
}
return shape;
}
Shape makeRect (const Vec2 & pos, float angle, float width, float height) {
// This is really not the best way to construct objects in C++, but I'm trying
// to stick to C / procedural style for this example.
Shape shape { pos, angle };
shape.type = Shape::Type::Rectangle;
shape.rect_data.width = width;
shape.rect_data.height = height;
return shape;
}
// etc...
// Going back to the OOP example, there's actually another good example of when to use inheiritance
// (run time overhead) vs templates / CRTP.
// Consider the Renderer class: ideally, we'd like to support multiple renderers that have the
// same shared interface. The Java-style approach would be to make the Renderer class be an interface,
// and support multiple renderers like so:
class IRenderer {
virtual void renderLine (Vec2 p1, Vec2 p2) = 0;
virtual void setColor (const Color & color) = 0;
// ...
};
class SoftwareRenderer : public IRenderer {
// ...
};
class GLRenderer : public IRenderer {
// ...
};
class DX9Renderer : public IRenderer {
// ...
};
// The only problem with this approach is that we're likely to only use *one* renderer type in our
// program, so we might as well just bake it in and avoid the virtual methods for a (minor) performance
// boost.
class Shape {
// ...
template <class Renderer>
virtual void render () {}
};
// This works just as well, and you can still code to a common *interface* w/ your renderer implementations;
// just its, uh... invisible, and not at all enforced by the compiler (so you could get a bazillion seemingly
// random compiler errors if you forgot to implement something in one of the renderer backends).
// C++11 was actually *supposed* to get a template version of interfaces via the Concepts feature (look it
// up), but it was scrapped because the standards committe is full of idiots and it was supposedly too hard
// to implement.
// Aside: the one caveat of using templates is that every template instantiation (eg. vector<int>, vector<float>,
// vector<map<string, int>>, etc), *generates its own code* – ie. everything is inlined. This is both a good and
// bad thing: on the plus side, inlining means that the resulting code is potentially faster and certainly better
// optimized than using shared code and dynamic typing to achieve the same (eg. Objective-C), but on the down side,
// this can cause a massive amount of code bloat that means longer compiler times, bigger executables, and
// potentially *slower* performance if the resulting code overflows the cache.
// Also, there actually is a benefit to using the first (OO / virtual inheiritance) approach for the renderer
// example: it enables you to hot-swap renderers while your program is running (very useful for some games),
// without either a) requiring a complete recompile, or b) including all the unused code paths and using switches
// (eg self-modifying asm) to swap out at run-time. Actually, the latter is kind of like the idea behind OSX
// universal binaries, which included code for multiple architectures (PPC and X86) in the same executable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment