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 |
|
---|