Skip to content

Commit 1016f2f

Browse files
More WIP how it works
1 parent 4cfaaf6 commit 1016f2f

File tree

1 file changed

+198
-10
lines changed

1 file changed

+198
-10
lines changed

docs/how_it_works.md

Lines changed: 198 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,127 @@
33

44
This document is meant to clearly show the principles on which archetype works without you having to decipher the macro heavy archetype.h file. The intention is to show how the principles have been combined, and provide a rationale for why I have implemented archetype this way.
55

6-
Underneath the hood Archetype uses manual vtables to acheive type erasure and run time polymorphism. This is not dissimilar from how virtual functions in traditional inheritance work. Vtables and surrounding infrastructure require a lot of boiler plate. The point of Archetype is to automate manual vtable generation, in a modular/composable way.
6+
Underneath the hood `Archetype` uses manual vtables to acheive type erasure and run time polymorphism. This is not dissimilar from how virtual functions in traditional inheritance work. Vtables and surrounding infrastructure require a lot of boiler plate. The point of `Archetype` is to automate manual `vtable` generation, in a modular/composable way.
77

8+
### The basic vtable
9+
A `vtable` is a structure containing free function pointers (non member function pointers). For example, this is a `vtable` structure containing function pointers for `func_a` and `func_b`.
10+
11+
```cpp
12+
struct vtable
13+
{
14+
int (*func_a)(int);
15+
float (*func_b)(float);
16+
};
17+
```
18+
We can now assign any function that matches this signature to the vtable. For example:
19+
20+
```cpp
21+
int func_a(int a) { return a + 5; }
22+
float func_b(float b) { return b + 5.3; }
23+
24+
vtable mytable;
25+
mytable.func_a = &func_a;
26+
mytable.func_b = &func_b;
27+
```
28+
we can now call these functions through the vtable:
29+
```cpp
30+
mytable.func_a(5); //returns 10;
31+
mytable.func_b(5.f); //returns 10.3;
32+
```
33+
### The object facing vtable
34+
In Archetype the goal is binding objects/classes, not free functions. Member function pointers (non static) are not free functions. They have to point to both the correct function, and the object instance, which mean they don't have the size of a free function pointer.
35+
36+
Lets say we want a vtable that can call objects of the following type:
37+
```cpp
38+
struct A
39+
{
40+
int func_a(int a) { return a + internal_int++; }
41+
float func_b(float b) { return b + (internal_float += 1.3f); }
42+
int internal_int = 5;
43+
int internal_float = 5.3;
44+
};
45+
```
46+
47+
We can get around this by storing free functions that can be called with an object pointer. For example our vtable becomes:
48+
49+
```cpp
50+
struct vtable
51+
{
52+
int (*func_a)(A *, int);
53+
float (*func_b)(A *, float);
54+
};
55+
```
56+
We can then assign this vtable to call an object of type `A's` functions.
57+
```cpp
58+
A obj;
59+
vtable vtbl;
60+
vtbl.func_a = [](A * ptr, int a) { return ptr->func_a(a); };
61+
vtbl.func_b = [](A * ptr, float b) { return ptr->func_b(b); };
62+
63+
vtbl.func_a(obj, 5); // call func_a on obj
64+
vtbl.func_b(obj, 6.4); // call func_b on obj
65+
```
66+
67+
This implementation is not very generic. As our vtable depends on type A. We can make the vtable type agnostic by passing in the object as a void pointer instead. We can push the type specific handling into the lambda.
68+
69+
```cpp
70+
struct vtable
71+
{
72+
int (*func_a)(void *, int);
73+
float (*func_b)(void *, float);
74+
};
75+
76+
vtbl.func_a = [](void * ptr, int a) {
77+
return static_cast<A*>(ptr)->func_a(a);
78+
};
79+
80+
vtbl.func_a(static_cast<void*>(obj), 5);
81+
```
82+
83+
### The view
84+
So far the vtable itself has given us a type erased way to call functions on arbitrary types, provided we can assign lambdas to out vtable function pointers. However, its still very awkward to setup and call. The `view` is an object that carries around the object `void *` and can pass this into the vtable functions.
85+
86+
```cpp
87+
struct view
88+
{
89+
int func_a(int a) { return vtbl_->func_a(obj_, a); }
90+
float func_b(float b) { return vtbl_->func_b(obj_, b); }
91+
void * obj_;
92+
vtable * vtbl_;
93+
};
94+
```
95+
Provided our vtable pointer is pointing to a vtable that is correct for the current type, then we can assign the object and call like so:
96+
97+
```cpp
98+
A obj;
99+
view myview;
100+
myview.obj_ = static_cast<void*>(&obj);
101+
myview.vtbl_ = vtbl; // vtable for A
102+
103+
myview.func_a(5);
104+
myview.func_b(3.2);
105+
```
106+
107+
This is a little cleaner. We have one vtable instance per bound type. And we have one view instance per object that we bind to. By keeping the vtable and view separate as separate objects we keep memory usage lower, and have improved cache locality for the function pointers.
108+
109+
To ensure we are only creating on vtable per type, we can make use of a static vtable variable within a templated function. Every time we call `make_vtable<A>()` we are using a pointer to the `A` `vtable`.
110+
111+
```cpp
112+
template <typename T>
113+
static vtable * make_vtable()
114+
{
115+
static vtable vtablet;
116+
vtablet.func_a = [](void * obj, int arg0) -> int {
117+
return static_cast<T*>(obj)->write(arg0);
118+
};
119+
...
120+
return &vtablet;
121+
}
122+
```
123+
To handle function overloading we need to make sure that the `vtable` uses unique names for its function pointers. We can use normal function overloading in the view to resolve the correct function call.
124+
125+
### The Archetype
126+
Taking inspiration from the way that C++20 concepts can be defined and composed together, I wanted to do something similar. The idea being that you could define interface specifications, and then compose these together. Below is the rough idea for a structure that defines the `writable` "concept". I ended up calling these containing structures Archetypes.
8127

9128
```cpp
10129
struct writable
@@ -42,29 +161,98 @@ struct writable
42161
};
43162
```
44163

45-
Making it composable:
164+
To create views to types `A` and `B` we can do the following:
46165

47166
```cpp
48-
struct VTableBase {
167+
A a; B b; B b2;
168+
writable::view view_a = writable::make_view(a);
169+
writable::view view_b = writable::make_view(b);
170+
writable::view view_b2 = writable::make_view(b2);
171+
```
172+
Both `view_b` and `view_b2` are using the same vtable to type `B`.
49173

50-
template<typename T>
51-
static VTableBase * make_vtable() {
52-
static VTableBase vtablet;
53-
return &vtablet;
54-
}
55174

56-
template <typename T>
57-
void bind() {}
175+
### The composable vtable
176+
177+
Lets say we also have a `readable` `Archetype`, and we would like to compose this together with `writable` to create `readwritable`. The resulting vtable and view should end up being:
178+
179+
```cpp
180+
struct readwritable
181+
{
182+
struct vtable
183+
{
184+
int (*write)(void * obj, const char *, int); // from writable
185+
int (*read)(void * obj, char *, int); // from readable
186+
};
187+
188+
struct view
189+
{
190+
void * obj; // from ?
191+
vtable * vtbl; // from ?
192+
int write(const char * arg0, int arg1) {
193+
return vtbl->write(obj, arg0, arg1); // from writable
194+
}
195+
int read(char * arg0, int arg1) {
196+
return vtbl->read(obj, arg0, arg1); // from readable
197+
}
198+
};
199+
...
200+
};
201+
```
202+
203+
I did this by orthogonalising the structures, and then composing them through inheritance of orthogonal parts. The orthogonal parts come from each of the existing archetypes, while the common parts can come from a common base.
204+
205+
#### The composable vtable
206+
```cpp
207+
208+
struct VTableCommonBase { }; // common base is empty as no common parts
209+
210+
// view table layer for writable
211+
template<typename BaseVTable = VTableCommonBase>
212+
struct write_vtlayer : public BaseVtable
213+
{
214+
int (*write)(void * obj, const char *, int);
215+
};
216+
217+
// view table layer for readable
218+
template<typename BaseVTable = VTableCommonBase>
219+
struct read_vtlayer : public BaseVtable
220+
{
221+
int (*read)(void * obj, char *, int);
58222
};
223+
```
224+
We can still create our normal readable and writable vtables as:
225+
226+
```cpp
227+
write_vtlayer<> wvtl;
228+
write_vtlayer<> rvtl;
229+
```
230+
We can now compose our inheritance chain for the readwritable vtable as:
231+
232+
```cpp
233+
template<typename BaseVTable = VTableCommonBase>
234+
struct readwrite_vtable_layer
235+
: public write_vtlayer<read_vtlayer<BaseVtable>> // inheritance chain
236+
{};
237+
```
238+
239+
#### The composable view
240+
We end up doing the same thing for the view. Except we have a common object, and vtable pointer, which gets placed in the common base.
59241

242+
```cpp
60243
template<typename VTableType>
61244
struct ViewBase
62245
{
63246
void * _obj;
64247
VTableType * _vtbl;
65248
};
249+
```
250+
66251

67252

253+
254+
255+
```
68256
struct writable
69257
{
70258
template<typename BaseVTable = VTableBase>

0 commit comments

Comments
 (0)