DEV Community

loading...

C++ For Go Programmers: Part 1 - Struct Embedding, Object Receivers, and Interfaces in C++

dayvonjersen profile image dayvonjersen Updated on ・5 min read

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

Discussion (0)

Forem Open with the Forem app