|
3 | 3 |
|
4 | 4 | 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. |
5 | 5 |
|
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. |
7 | 7 |
|
| 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. |
8 | 127 |
|
9 | 128 | ```cpp |
10 | 129 | struct writable |
@@ -42,29 +161,158 @@ struct writable |
42 | 161 | }; |
43 | 162 | ``` |
44 | 163 |
|
45 | | -Making it composable: |
| 164 | +To create views to types `A` and `B` we can do the following: |
46 | 165 |
|
47 | 166 | ```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`. |
49 | 173 |
|
50 | | - template<typename T> |
51 | | - static VTableBase * make_vtable() { |
52 | | - static VTableBase vtablet; |
53 | | - return &vtablet; |
54 | | - } |
55 | 174 |
|
56 | | - template <typename T> |
57 | | - void bind() {} |
| 175 | +### The composable view |
| 176 | + |
| 177 | +The view is a little easier to make composable than the vtable so I'll start here. Lets say we also have a `readable` `Archetype`, and we would like to compose this together with `writable` to create `readwritable`. The view should end up being: |
| 178 | + |
| 179 | +```cpp |
| 180 | +struct readwritable |
| 181 | +{ |
| 182 | + struct view |
| 183 | + { |
| 184 | + void * obj; // from ? |
| 185 | + vtable * vtbl; // from ? |
| 186 | + int write(const char * arg0, int arg1) { |
| 187 | + return vtbl->write(obj, arg0, arg1); // from writable |
| 188 | + } |
| 189 | + int read(char * arg0, int arg1) { |
| 190 | + return vtbl->read(obj, arg0, arg1); // from readable |
| 191 | + } |
| 192 | + }; |
| 193 | + ... |
58 | 194 | }; |
| 195 | +``` |
59 | 196 |
|
| 197 | +In Archetype 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. `readable` annd `writable` views define the `read()` and `write()` functions respectively. But they will need to share a common vtable, and void object pointer. We will see how to define this vtable later. Both the object pointer and the vtable pointer get placed in the common base. |
| 198 | + |
| 199 | +```cpp |
60 | 200 | template<typename VTableType> |
61 | | -struct ViewBase |
| 201 | +struct view_base |
62 | 202 | { |
63 | 203 | void * _obj; |
64 | 204 | VTableType * _vtbl; |
65 | 205 | }; |
| 206 | +``` |
| 207 | + |
| 208 | +The readable and writable views are orthogonalised into layers which can bve composed in an inheritance chain. For now you can ignore the default assignment of `BaseViewLayer = view_base<vtable<>>`, `vtable<>` will be discussed in a later section. |
| 209 | +```cpp |
| 210 | +struct writable |
| 211 | +{ |
| 212 | + template<typename BaseViewLayer = view_base<vtable<>>> |
| 213 | + struct view_layer : public BaseViewLayer |
| 214 | + { |
| 215 | + using BaseViewLayer::_obj; |
| 216 | + using BaseViewLayer::_vtbl; |
| 217 | + |
| 218 | + int write(const char * arg0, int arg1) { |
| 219 | + return _vtbl->write(_obj, arg0, arg1); |
| 220 | + } |
| 221 | + }; |
| 222 | + ... |
| 223 | +}; |
66 | 224 |
|
| 225 | +struct readable |
| 226 | +{ |
| 227 | + template<typename BaseViewLayer = view_base<vtable<>>> |
| 228 | + struct view_layer : public BaseViewLayer |
| 229 | + { |
| 230 | + using BaseViewLayer::_obj; |
| 231 | + using BaseViewLayer::_vtbl; |
| 232 | + |
| 233 | + int read(char * arg0, int arg1) { |
| 234 | + return _vtbl->read(_obj, arg0, arg1); |
| 235 | + } |
| 236 | + }; |
| 237 | + ... |
| 238 | +}; |
| 239 | + |
| 240 | +struct readwritable |
| 241 | +{ |
| 242 | + template<typename BaseViewLayer = view_base<vtable<>>> |
| 243 | + struct view_layer : public writable::view_layer<readable::view_layer<BaseViewLayer>> |
| 244 | + { |
| 245 | + using BaseViewLayer::_obj; |
| 246 | + using BaseViewLayer::_vtbl; |
| 247 | + }; |
| 248 | + ... |
| 249 | +}; |
| 250 | +``` |
| 251 | + |
| 252 | +The default parameterisation of the view layers with `view_base<vtable<>>` will set the `BaseViewLayer::_vtbl` to use the correct `vtable` type when no parameters are passed in. For example, here is how we can instantiate concrete views for each Archetype. |
| 253 | + |
| 254 | +```cpp |
| 255 | +writable::view_layer<> write_view; |
| 256 | +readable::view_layer<> readable_view; |
| 257 | +readwritable::view_layer<> readable_view; |
| 258 | +``` |
| 259 | + |
| 260 | +While we have ended up with something more complex and verbose we are making progress in the right direction. The structures generated are more modular, and homogenous, which later becomes important for automating their generation and composition. |
| 261 | + |
| 262 | + |
| 263 | +#### The composable vtable |
| 264 | +The composable vtable follows the same principals as the composable view. All the common parts go in the base, all the orthogonal parts go in the layers. |
| 265 | +A first approach would look something like this: |
| 266 | + |
| 267 | +```cpp |
| 268 | +struct vtable_base { }; // common base is empty as no common parts |
67 | 269 |
|
| 270 | +struct writable |
| 271 | +{ |
| 272 | + template<typename BaseVTable = vtable_base> |
| 273 | + struct vtable : public BaseVTable |
| 274 | + { |
| 275 | + int (*write)(void * obj, const char *, int); |
| 276 | + }; |
| 277 | + ... |
| 278 | +}; |
| 279 | + |
| 280 | +struct readable |
| 281 | +{ |
| 282 | + template<typename BaseVTable = vtable_base> |
| 283 | + struct vtable : public BaseVTable |
| 284 | + { |
| 285 | + int (*read)(void * obj, char *, int); |
| 286 | + }; |
| 287 | + ... |
| 288 | +}; |
| 289 | + |
| 290 | +struct readwritable |
| 291 | +{ |
| 292 | + template<typename BaseVTable = vtable_base> |
| 293 | + struct vtable : public writable::vtable<readable::vtable<BaseVTable>> { }; |
| 294 | + ... |
| 295 | +}; |
| 296 | +``` |
| 297 | +We can still create our normal readable and writable vtables as: |
| 298 | + |
| 299 | +```cpp |
| 300 | +writable::vtable<> wvtl; |
| 301 | +readable::vtable<> rvtl; |
| 302 | +readwritable::vtable<> rvtl; |
| 303 | +``` |
| 304 | + |
| 305 | +The problem we have now, is that while we can create function pointers for each vtable, we still need a way to assign them. |
| 306 | +Becase we are automating this, we need an orthogonal way to do this. Because each derived class knows the |
| 307 | + |
| 308 | + |
| 309 | + |
| 310 | + |
| 311 | + |
| 312 | + |
| 313 | + |
| 314 | + |
| 315 | +```cpp |
68 | 316 | struct writable |
69 | 317 | { |
70 | 318 | template<typename BaseVTable = VTableBase> |
|
0 commit comments