Changeset d49bfa8 for doc/proposals
- Timestamp:
- Jul 20, 2017, 2:05:31 PM (7 years ago)
- Branches:
- ADT, aaron-thesis, arm-eh, ast-experimental, cleanup-dtors, deferred_resn, demangler, enum, forall-pointer-decay, jacob/cs343-translation, jenkins-sandbox, master, new-ast, new-ast-unique-expr, new-env, no_list, persistent-indexer, pthread-emulation, qualifiedEnum, resolv-new, with_gc
- Children:
- 957453d
- Parents:
- ac10576
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
doc/proposals/virtual.txt
rac10576 rd49bfa8 1 1 Proposal for virtual functionality 2 3 There are two types of virtual inheritance in this proposal, relaxed 4 (implicate) and strict (explicate). Relaxed is the simpler case that uses the 5 existing trait system with the addition of trait references and vtables. 6 Strict adds some constrants and requires some additional notation but allows 7 for down-casting. 8 9 Relaxed Virtual Inheritance: 2 10 3 11 Imagine the following code : … … 20 28 void draw(line*); 21 29 22 While all the members of this simple UI support drawing creating a UI that easily23 supports both these UI requires some tedious boiler-plate code :30 While all the members of this simple UI support drawing creating a UI that 31 easily supports both these UI requires some tedious boiler-plate code : 24 32 25 33 enum type_t { text, line }; … … 41 49 } 42 50 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]; 51 While this code will work as indented, adding any new widgets or any new 52 widget behaviors requires changing existing code to add the desired 53 functionality. To ease this maintenance effort required CFA introduces the 54 concept of trait references. 55 56 Using trait references to implement the above gives the following : 57 58 trait drawable objects[10]; 50 59 fill_objects(objects); 51 60 52 61 while(running) { 53 for(drawable *object : objects) {62 for(drawable object : objects) { 54 63 draw(object); 55 64 } 56 65 } 57 66 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: 67 The keyword trait is optional (by the same rules as the struct keyword). This 68 is not currently suported in CFA and the lookup is not possible to implement 69 statically. Therefore we need to add a new feature to handle having dynamic 70 lookups like this. 71 72 What we really want to do is express the fact that calling draw() on a trait 73 reference should find the underlying type of the given parameter and find how 74 it implements the routine, as in the example with the enumeration and union. 75 76 For instance specifying that the drawable trait reference looks up the type 77 of the first argument to find the implemenation would be : 70 78 71 79 trait drawable(otype T) { … … 73 81 }; 74 82 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 83 This could be implied in simple cases like this one (single parameter on the 84 trait and single generic parameter on the function). In more complex cases it 85 would have to be explicately given, or a strong convention would have to be 86 enforced. 87 88 Once a function in a trait has been marked as virtual it defines a new 89 function that takes in that trait's reference and then dynamically calls the 90 underlying type implemenation. Hence a trait reference becomes a kind of 91 abstract type, cannot be directly instantiated but can still be used. 92 93 One of the limitations of this design is that it does not support double 94 dispatching, which concretely means traits cannot have routines with more than 95 one virtual parameter. The program must have a single table to look up the 96 function on. Using trait references with traits with more than one parameter 97 is also restricted, inistially forbidden, see extention. 98 99 Extention: Multi-parameter Virtual Traits: 100 101 This implementation can be extented to traits with multiple parameters if 102 one is called out as being the virtual trait. For example : 103 104 trait iterator(otype T, dtype Item) { 105 Maybe(Item) next(virtual T *); 106 } 107 108 iterator(int) generators[10]; 109 110 Which creates a collection of iterators that produce integers, regardless of 111 how those iterators are implemented. This may require a note that this trait 112 is virtual on T and not Item, but noting it on the functions may be enough. 113 114 115 Strict Virtual Inheritance: 116 117 One powerful feature relaxed virtual does not support is the idea of down 118 casting. Once something has been converted into a trait reference there is 119 very little we can do to recover and of the type information, only the trait's 120 required function implementations are kept. 121 122 To allow down casting strict virtual requires that all traits and structures 123 involved be orginized into a tree. Each trait or struct must have a unique 124 position on this tree (no multiple inheritance). 125 126 This is declared as follows : 127 128 trait error(otype T) virtual { 129 const char * msg(T *); 130 } 131 132 trait io_error(otype T) virtual error { 133 FILE * src(T *); 134 } 135 136 struct eof_error virtual io_error { 137 FILE * fd; 138 }; 139 140 So the trait error is the head of a new tree and io_error is a child of it. 141 142 Also the parent trait is implicately part of the assertions of the children, 143 so all children implement the same operations as the parent. By the unique 144 path down the tree, we can also uniquely order them so that prefixs of a 145 child's vtable has the same format as the parent's. 146 147 This gives us an important extra feature, runtime checking of the parent-child 148 relationship with a C++ dynamic_cast like operation. Allowing checked 149 convertions from trait references to more particular references, which works 150 if the underlying type is, or is a child of, the new trait type. 151 152 Extention: Multiple Parents 153 154 Although each trait/struct must have a unique position on each tree, it could 155 have positions on multiple trees. All this requires is the ability to give 156 multiple parents, as here : 157 158 trait region(otype T) virtual drawable, collider; 159 160 The restriction being, the parents must come from different trees. This 161 object (and all of its children) can be cast to either tree. This is handled 162 by generating a seperate vtable for each tree the structure is in. 163 164 Extention: Multi-parameter Strict Virtual 165 166 If a trait has multiple parameters than one must be called out to be the one 167 we generate seperate vtables for, as in : 168 169 trait example(otype T, otype U) virtual(T) ... 170 171 This can generate a seperate vtable for each U for which all the T+U 172 implementations are provided. These are then seperate nodes in the tree (or 173 the root of different trees) as if each was created individually. 174 175 Example: 176 177 trait argument(otype T) virtual { 178 char short_name(virtual T *); 179 bool is_set(virtual T *); 180 }; 181 182 trait value_argument(otype T, otype U) virtual(T) argument { 183 U get_value(virtual T *); 184 }; 185 186 Extention: Structural Inheritance 187 188 Currently traits must be the internal nodes and structs the leaf nodes. 189 Structs could be made internal nodes as well, and might specify structural 190 inheritance. 191 192 193 Storing the Virtual Lookup Table (vtable): 194 195 We have so fare been silent on how the vtable is created, stored and accessed. 196 197 Creation happens at compile time. Function pointers are found by using the 198 same best match rules as elsewhere (additional rules for defaults from the 199 parent may or may not be reqired). For strict virtual this must happen at the 200 global scope and forbiding static functions, to ensure that a single unique 201 vtable is created. Similarly, there may have to be stricter matching rules 202 for the functions that go into the vtable, possibly requiring an exact match. 203 Relaxed virtual could relax both restrictions, if we allow different vtable 204 at different conversion (struct to trait reference) sites. If it is allowed 205 local functions being bound to a vtable could cause issues when they go out 206 of scope, however this should follow the lifetime rules most C programs 207 already follow implicately. 208 209 Most vtables should be stored statically, the only exception being some of 210 the relaxed vtables that could have local function pointers. These may be able 211 to be stack allocated. All vtables should be immutable and require no manual 212 cleanup. 213 214 Access has two main options: 215 216 The first is through the use of fat pointers, or a tuple of pointers. When the 217 object is converted to a trait reference, the pointers to its vtables are 218 stored along side it. 219 220 This allows for compatability with existing structures (such as those imported 221 from C) and is the default storage method unless a different one is given. 222 223 The other is by inlining the vtable pointer as "intrusive vtables". This adds 224 a field to the structure to the vtable. The trait reference than has a single 225 pointer to this field, the vtable includes an offset to find the beginning of 226 the structure again. 227 228 This is used if you specify a vtable field in the structure. If given in the 229 trait the vtable pointer in the trait reference can then become a single 230 pointer to the vtable field and use that to recover the original object 231 pointer as well as retrive all operations. 232 233 trait drawable(otype T) { 234 vtable drawable; 235 }; 236 237 struct line { 238 vtable drawable; 239 vec2 start; 240 vec2 end; 241 }; 242 243 This inline code allows trait references to be converted to plain pointers 244 (although they still must be called specially). The vtable field may just be 245 an opaque block of memory or it may allow user access to the vtable. If so 246 than there should be some way to retrive the type of the vtable, which will be 247 autogenerated and often unique. 248 249 250 Keyword Usage: 251 252 It may be desirable to add fewer new keywords than discussed in this proposal. 253 It is possible that "virtual" could replace both "vtable" above with 254 unambiguous contextual meaning. However, for purposes of clarity in the design 255 discussion it is beneficial to keep the keywords for separate concepts distinct. 256 257 258 Trait References and Operations: 259 260 sizeof(drawable) will return the size of the trait object itself. However : 261 262 line a_line; 263 drawable widget = a_line; 264 sizeof(widget); 265 266 Will instead return the sizeof the underlying object, although the trait must 267 require that its implementation is sized for there to be a meaningful value 268 to return. You may also get the size of the trait reference with 269 270 sizeof(&widget); 271 272 Calling free on a trait reference will free the memory for the object. It will 273 leave the vtables alone, as those are (always?) statically allocated.
Note: See TracChangeset
for help on using the changeset viewer.