[63c2bca] | 1 | Proposal for virtual functionality |
---|
| 2 | |
---|
| 3 | Imagine the following code : |
---|
| 4 | |
---|
| 5 | trait drawable(otype T) { |
---|
| 6 | void draw(T* ); |
---|
| 7 | }; |
---|
| 8 | |
---|
| 9 | struct text { |
---|
| 10 | char* text; |
---|
| 11 | }; |
---|
| 12 | |
---|
| 13 | void draw(text*); |
---|
| 14 | |
---|
| 15 | struct line{ |
---|
| 16 | vec2 start; |
---|
| 17 | vec2 end; |
---|
| 18 | }; |
---|
| 19 | |
---|
| 20 | void draw(line*); |
---|
| 21 | |
---|
| 22 | While all the members of this simple UI support drawing creating a UI that easily |
---|
| 23 | supports both these UI requires some tedious boiler-plate code : |
---|
| 24 | |
---|
| 25 | enum type_t { text, line }; |
---|
| 26 | |
---|
| 27 | struct widget { |
---|
| 28 | type_t type; |
---|
| 29 | union { |
---|
| 30 | text t; |
---|
| 31 | line l; |
---|
| 32 | }; |
---|
| 33 | }; |
---|
| 34 | |
---|
| 35 | void draw(widget* w) { |
---|
| 36 | switch(w->type) { |
---|
| 37 | case text : draw(&w->text); break; |
---|
[5a0735ac] | 38 | case line : draw(&w->line); break; |
---|
[63c2bca] | 39 | default : handle_error(); break; |
---|
| 40 | } |
---|
| 41 | } |
---|
| 42 | |
---|
| 43 | While this code will work as indented, adding any new widgets or any new widget behaviors |
---|
| 44 | requires changing existing code to add the desired functionality. To ease this maintenance |
---|
| 45 | effort required CFA introduces the concept of dynamic types, in a manner similar to C++. |
---|
| 46 | |
---|
| 47 | A simple usage of dynamic type with the previous example would look like : |
---|
| 48 | |
---|
| 49 | drawable* objects[10]; |
---|
| 50 | fill_objects(objects); |
---|
| 51 | |
---|
| 52 | while(running) { |
---|
| 53 | for(drawable* object : objects) { |
---|
| 54 | draw(object); |
---|
| 55 | } |
---|
| 56 | } |
---|
| 57 | |
---|
| 58 | However, this is not currently do-able in the current CFA and furthermore is not |
---|
| 59 | possible to implement statically. Therefore we need to add a new feature to handle |
---|
| 60 | having dynamic types like this (That is types that are found dynamically not types |
---|
| 61 | that change dynamically). |
---|
| 62 | |
---|
| 63 | C++ uses inheritance and virtual functions to find the |
---|
| 64 | desired type dynamically. CFA takes inspiration from this solution. |
---|
| 65 | |
---|
| 66 | What we really want to do is express the fact that calling draw() on a object |
---|
| 67 | should find the dynamic type of the parameter before calling the routine, much like the |
---|
| 68 | hand written example given above. We can express this by adding the virtual keyword on |
---|
| 69 | the parameter of the constraints on our trait: |
---|
| 70 | |
---|
| 71 | trait drawable(otype T) { |
---|
| 72 | void draw(virtual T* ); |
---|
| 73 | }; |
---|
| 74 | |
---|
| 75 | This expresses the idea that drawable is similar to an abstract base class in C++ and |
---|
| 76 | also gives meaning to trying to take a pointer of drawable. That is anything that can |
---|
| 77 | be cast to a drawable pointer has the necessary information to call the draw routine on |
---|
| 78 | that type. Before that drawable was only a abstract type while now it also points to a |
---|
| 79 | piece of storage which specify which behavior the object will have at run time. |
---|
| 80 | |
---|
| 81 | This storage needs to be allocate somewhere. C++ just adds an invisible pointer at |
---|
| 82 | the beginning of the struct but we can do something more explicit for users, actually |
---|
| 83 | have a visible special field : |
---|
| 84 | |
---|
| 85 | struct text { |
---|
| 86 | char* text; |
---|
| 87 | vtable drawable; |
---|
| 88 | }; |
---|
| 89 | |
---|
| 90 | struct line{ |
---|
| 91 | vtable drawable; |
---|
| 92 | vec2 start; |
---|
| 93 | vec2 end; |
---|
| 94 | }; |
---|
| 95 | |
---|
| 96 | With these semantics, adding a "vtable drawable" means that text pointers and line pointers are now |
---|
| 97 | convertible to drawable pointers. This conversion will not necessarily be a type only change however, indeed, |
---|
| 98 | the drawable pointer will point to the field "vtable drawable" not the head of the struct. However, since all |
---|
| 99 | the types are known at compile time, converting pointers becomes a simple offset operations. |
---|
| 100 | |
---|
| 101 | The vtable field contains a pointer to a vtable which contains all the information needed for the caller |
---|
| 102 | to find the function pointer of the desired behavior. |
---|
| 103 | |
---|
| 104 | One of the limitations of this design is that it does not support double dispatching, which |
---|
| 105 | concretely means traits cannot have routines with more than one virtual parameter. This design |
---|
[ba5131d] | 106 | would have many ambiguities if it did support multiple virtual parameter. A futher limitation is |
---|
| 107 | that traits over more than one type cannot have vtables meaningfully defined for them, as the |
---|
| 108 | particular vtable to use would be a function of the other type(s) the trait is defined over. |
---|
[63c2bca] | 109 | |
---|
[da81e1d0] | 110 | It is worth noting that the function pointers in these vtables are bound at object construction, rather than |
---|
| 111 | function call-site, as in Cforall's existing polymorphic functions. As such, it is possible that two objects |
---|
| 112 | with the same static type would have a different vtable (consider what happens if draw(line*) is overridden |
---|
| 113 | between the definitions of two line objects). Given that the virtual drawable* erases static types though, |
---|
| 114 | this should not be confusing in practice. A more distressing possibility is that of creating an object that |
---|
| 115 | outlives the scope of one of the functions in its vtable. This is certainly a possible bug, but it is of a |
---|
| 116 | type that C programmers are familiar with, and should be able to avoid by the usual methods. |
---|
| 117 | |
---|
[63c2bca] | 118 | Extensibility. |
---|
| 119 | |
---|
| 120 | One of the obvious critics of this implementation is that it lacks extensibility for classes |
---|
| 121 | that cannot be modified (ex: Linux C headers). However this solution can be extended to |
---|
| 122 | allow more extensibility by adding "Fat pointers". |
---|
| 123 | |
---|
| 124 | Indeed, users could already "solve" this issue by writing their own fat pointers as such: |
---|
| 125 | |
---|
| 126 | trait MyContext(otype T) { |
---|
| 127 | void* get_stack(virtual T*) |
---|
| 128 | }; |
---|
| 129 | |
---|
| 130 | void* get_stack(ucontext_t *context); |
---|
| 131 | |
---|
| 132 | struct fat_ucontext_t { |
---|
| 133 | vtable MyContext; |
---|
| 134 | ucontext_t *context; |
---|
| 135 | } |
---|
| 136 | |
---|
| 137 | //Tedious forwarding routine |
---|
| 138 | void* get_stack(fat_ucontext_t *ptr) { |
---|
| 139 | return get_stack(ptr->context); |
---|
| 140 | } |
---|
| 141 | |
---|
| 142 | However, users would have to write all the virtual methods they want to override and make |
---|
| 143 | them all simply forward to the existing method that takes the corresponding POCO(Plain Old C Object). |
---|
| 144 | |
---|
| 145 | The alternative we propose is to use language level fat pointers : |
---|
| 146 | |
---|
| 147 | trait MyContext(otype T) { |
---|
| 148 | void* get_stack(virtual T*) |
---|
| 149 | }; |
---|
| 150 | |
---|
| 151 | void* get_stack(ucontext_t *context); |
---|
| 152 | |
---|
| 153 | //The type vptr(ucontext_t) all |
---|
| 154 | vptr(ucontext_t) context; |
---|
| 155 | |
---|
| 156 | These behave exactly as the previous example but all the forwarding routines are automatically generated. |
---|
[da81e1d0] | 157 | |
---|
| 158 | Bikeshedding. |
---|
| 159 | |
---|
| 160 | It may be desirable to add fewer new keywords than discussed in this proposal; it is possible that "virtual" |
---|
| 161 | could replace both "vtable" and "vptr" above with unambiguous contextual meaning. However, for purposes of |
---|
| 162 | clarity in the design discussion it is beneficial to keep the keywords for separate concepts distinct. |
---|
| 163 | |
---|