DISCLAIMER: This series title is inspired by a terrific article on the Go Wiki but, unlike that article, none of the information presented in this series should in any way be taken as sound advice. The author presumes a core competency in both modern C++ and Go on the part of the reader which exceeds the author's own. It is a thinly veiled call for help from a disturbed individual with too much time on his hands and an irrational fear of traditional OOP. See Part 0 for an introduction.
Struct Embedding
Go provides struct embedding as the only an alternative to inheritance:
package main
type parent struct {
something int
}
type child struct {
parent
somethingElse float64
}
func main() {
c := child{}
c.something = 42
}
But if you fmt.Printf("%#v", c)
you'd see how this actually works:
main.child{
parent:main.parent{something:42},
somethingElse:0
}
And indeed you can access something
by doing c.parent.something
as well.
Therefore, on a conceptual level at least, the C++ equivalent is:
struct parent {
int something;
};
struct child {
parent parent;
double somethingElse;
};
int main() {
child c = {0};
c.parent.something = 42;
return 0;
}
The same holds true for C, barring some typedef
's and this is nothing new. C programmers do this sort of thing all the time.
Ergo, we can have inherited members by using struct fields.
Object Receivers
Go allows us to define methods on our defined types. Not only with structs, but let's keep things simple.
type myStruct struct {
something int
}
func (s *myStruct) Method() {
s.something = 42
}
The object receiver is of a pointer type, rather than as a value in order to effect changes when called
s1 := &myStruct{}
s1.Method() // s1.something == 42
Conceptually, this is equivalent to:
func Method(s* myStruct) {
s.something = 42
}
And indeed the C/C++ equivalent is something very commonly found in C code:
struct my_struct {
int something;
};
my_struct_method(my_struct* s) {
s->something = 42;
}
Object receivers as the first argument to a function which acts on a struct is the defacto way of declaring "methods" on an "object" in C and is valid C++. This is nothing new.
Interfaces
Let's combine what we've established so far and do something completely bananas.
Go's interface model, to paraphrase Commander Pike, is
If an object has all the methods of an interface, then it satisfies that interface.
Have some code:
#include <stdlib.h>
#include <functional>
#include <iostream>
#include <vector>
struct some_interface {
std::function<void()>* Method;
};
some_interface* new_some_interface() {
return (some_interface*) calloc(1, sizeof(some_interface));
}
struct parent {
some_interface* some_interface;
};
parent* new_parent() {
parent* p = (parent*) calloc(1, sizeof(parent));
p->some_interface = nullptr;
return p;
}
struct child {
parent* parent;
int something;
};
child* new_child() {
return (child*) calloc(1, sizeof(child));
}
child_method(child* c) {
c->something = 42;
}
int main() {
child* c1 = new_child();
child* c2 = new_child();
c1->parent = new_parent();
c2->parent = new_parent();
c1->parent->some_interface = new_some_interface();
c1->parent->some_interface->Method = new std::function<void()>([=]() -> void {
child_method(c1);
});
std::vector<parent*> parents;
parents.push_back(c1->parent);
parents.push_back(c2->parent);
for(auto p : parents) {
if(p->some_interface != nullptr) {
(*p->some_interface->Method)();
}
}
std::cout << "c1.something: " << c1->something << "\n";
std::cout << "c2.something: " << c2->something << "\n";
return 0;
}
Now if you're still with me and you haven't closed the browser tab in utter disgust let me explain all this and why I think it's easier to think about than polymorphism and inheritance (even if it's much harder to look at).
How it works
The parent
is an ancestor, equivalent to an abstract base class in some ways. It holds pointers to all the interfaces that "subclasses" might have, but unlike abstract virtual functions, all of those methods do not necessarily have to be defined by subclasses. When calling methods on a parent, one simply checks if the interface has been defined first before calling the desired method.
parent
's are "inherited" by subclasses such as child
, but one could imagine step_child
and adopted_orphan
having a parent
field as well. parent
's are what are passed around because they expose the interface their children
define.
Those definitions come in the form of C++ lambda
functions new in c++11! because they are closures which can capture variables in the surrounding scope.
But they can just as easily be regular C function pointers just that that requires casting back and forth from void* to an explicit type:
// sorry that this example is so different from the one above but it's what i had on hand
#include <stdio.h>
typedef struct _interface {
int(*CommonMethod)(void*);
} _interface;
typedef struct object_a {
int prop;
_interface iface;
} object_a;
typedef struct object_b {
int different_prop;
_interface iface;
} object_b;
void func_that_takes_interface(void* obj, _interface iface) {
printf("%d\n", iface.CommonMethod(obj));
}
int object_a_common_method(void* obj) {
object_a* a = (object_a*)obj;
return a->prop * a->prop;
}
int object_b_common_method(void* obj) {
object_b* b = (object_b*)obj;
return b->different_prop * 2;
}
int main() {
object_a a = {0};
object_b b = {0};
a.iface.CommonMethod = object_a_common_method;
b.iface.CommonMethod = object_b_common_method;
func_that_takes_interface((void*)&a, a.iface);
func_that_takes_interface((void*)&b, b.iface);
a.prop = 9;
b.different_prop = 9;
func_that_takes_interface((void*)&a, a.iface);
func_that_takes_interface((void*)&b, b.iface);
a.prop = 4;
b.different_prop = 10;
func_that_takes_interface((void*)&a, a.iface);
func_that_takes_interface((void*)&b, b.iface);
return 0;
}
It should be noted that C++
lambda
's not only let you forgo this inconvenience but let you define a function body inline (which is what they were actually designed for...) but that goes against what I'm about to say next:
In either case, we can provide a regular function that takes an object receiver
to serve as the function which satisfies a interface method. In this way, interfaces are not required to interface with a particular method that acts on a set of data but can be used when such situations arise that the sets of data, though with similar methods defined are of differing types.
Implementation details
std::function<>
is necessary to hold pointers to lambda
's. The one defined above is actually of type main::lambda<void()>
.
Heap allocations are necessary even in this toy example.
The corresponding free
functions are omitted for clarity above but are necessary nonetheless:
free_parent(parent* p) {
if(p->some_interface != nullptr) {
delete p->some_interface->Method;
}
free(p);
}
free_child(child* c) {
free_parent(c->parent);
free(c);
}
In Practise
I'm making a game. Every player
, enemy
, and npc
has an entity
struct field. That entity has interfaces such as renderable
and updateable
which are called by the game loop. It's working as intended so far. I need to add collidable
and interactable
next but I'm too busy being a nodev
Top comments (0)