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; |
---|
38 | case line : draw(&w->line); break; |
---|
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 |
---|
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. |
---|
109 | |
---|
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 | |
---|
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. |
---|
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 | |
---|