🛠️🐜 Antkeeper superbuild with dependencies included https://antkeeper.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

359 lines
12 KiB

  1. # Crash Course: poly
  2. <!--
  3. @cond TURN_OFF_DOXYGEN
  4. -->
  5. # Table of Contents
  6. * [Introduction](#introduction)
  7. * [Other libraries](#other-libraries)
  8. * [Concept and implementation](#concept-and-implementation)
  9. * [Deduced interface](#deduced-interface)
  10. * [Defined interface](#defined-interface)
  11. * [Fulfill a concept](#fulfill-a-concept)
  12. * [Inheritance](#inheritance)
  13. * [Static polymorphism in the wild](#static-polymorphism-in-the-wild)
  14. * [Storage size and alignment requirement](#storage-size-and-alignment-requirement)
  15. <!--
  16. @endcond TURN_OFF_DOXYGEN
  17. -->
  18. # Introduction
  19. Static polymorphism is a very powerful tool in C++, albeit sometimes cumbersome
  20. to obtain.<br/>
  21. This module aims to make it simple and easy to use.
  22. The library allows to define _concepts_ as interfaces to fulfill with concrete
  23. classes without having to inherit from a common base.<br/>
  24. This is, among others, one of the advantages of static polymorphism in general
  25. and of a generic wrapper like that offered by the `poly` class template in
  26. particular.<br/>
  27. What users get is an object that can be passed around as such and not through a
  28. reference or a pointer, as happens when it comes to working with dynamic
  29. polymorphism.
  30. Since the `poly` class template makes use of `entt::any` internally, it also
  31. supports most of its feature. Among the most important, the possibility to
  32. create aliases to existing and thus unmanaged objects. This allows users to
  33. exploit the static polymorphism while maintaining ownership of objects.<br/>
  34. Likewise, the `poly` class template also benefits from the small buffer
  35. optimization offered by the `entt::any` class and therefore minimizes the number
  36. of allocations, avoiding them altogether where possible.
  37. ## Other libraries
  38. There are some very interesting libraries regarding static polymorphism.<br/>
  39. Among all, the two that I prefer are:
  40. * [`dyno`](https://github.com/ldionne/dyno): runtime polymorphism done right.
  41. * [`Poly`](https://github.com/facebook/folly/blob/master/folly/docs/Poly.md):
  42. a class template that makes it easy to define a type-erasing polymorphic
  43. object wrapper.
  44. The former is admittedly an experimental library, with many interesting ideas.
  45. I've some doubts about the usefulness of some feature in real world projects,
  46. but perhaps my lack of experience comes into play here. In my opinion, its only
  47. flaw is the API which I find slightly more cumbersome than other solutions.<br/>
  48. The latter was undoubtedly a source of inspiration for this module, although I
  49. opted for different choices in the implementation of both the final API and some
  50. feature.
  51. Either way, the authors are gurus of the C++ community, people I only have to
  52. learn from.
  53. # Concept and implementation
  54. The first thing to do to create a _type-erasing polymorphic object wrapper_ (to
  55. use the terminology introduced by Eric Niebler) is to define a _concept_ that
  56. types will have to adhere to.<br/>
  57. For this purpose, the library offers a single class that supports both deduced
  58. and fully defined interfaces. Although having interfaces deduced automatically
  59. is convenient and allows users to write less code in most cases, this has some
  60. limitations and it's therefore useful to be able to get around the deduction by
  61. providing a custom definition for the static virtual table.
  62. Once the interface is defined, it will be sufficient to provide a generic
  63. implementation to fulfill the concept.<br/>
  64. Also in this case, the library allows customizations based on types or families
  65. of types, so as to be able to go beyond the generic case where necessary.
  66. ## Deduced interface
  67. This is how a concept with a deduced interface is introduced:
  68. ```cpp
  69. struct Drawable: entt::type_list<> {
  70. template<typename Base>
  71. struct type: Base {
  72. void draw() { this->template invoke<0>(*this); }
  73. };
  74. // ...
  75. };
  76. ```
  77. It's recognizable by the fact that it inherits from an empty type list.<br/>
  78. Functions can also be const, accept any number of parameters and return a type
  79. other than `void`:
  80. ```cpp
  81. struct Drawable: entt::type_list<> {
  82. template<typename Base>
  83. struct type: Base {
  84. bool draw(int pt) const { return this->template invoke<0>(*this, pt); }
  85. };
  86. // ...
  87. };
  88. ```
  89. In this case, all parameters must be passed to `invoke` after the reference to
  90. `this` and the return value is whatever the internal call returns.<br/>
  91. As for `invoke`, this is a name that is injected into the _concept_ through
  92. `Base`, from which one must necessarily inherit. Since it's also a dependent
  93. name, the `this-> template` form is unfortunately necessary due to the rules of
  94. the language. However, there exists also an alternative that goes through an
  95. external call:
  96. ```cpp
  97. struct Drawable: entt::type_list<> {
  98. template<typename Base>
  99. struct type: Base {
  100. void draw() const { entt::poly_call<0>(*this); }
  101. };
  102. // ...
  103. };
  104. ```
  105. Once the _concept_ is defined, users must provide a generic implementation of it
  106. in order to tell the system how any type can satisfy its requirements. This is
  107. done via an alias template within the concept itself.<br/>
  108. The index passed as a template parameter to either `invoke` or `poly_call`
  109. refers to how this alias is defined.
  110. ## Defined interface
  111. A fully defined concept is no different to one for which the interface is
  112. deduced, with the only difference that the list of types is not empty this time:
  113. ```cpp
  114. struct Drawable: entt::type_list<void()> {
  115. template<typename Base>
  116. struct type: Base {
  117. void draw() { entt::poly_call<0>(*this); }
  118. };
  119. // ...
  120. };
  121. ```
  122. Again, parameters and return values other than `void` are allowed. Also, the
  123. function type must be const when the method to bind to it is const:
  124. ```cpp
  125. struct Drawable: entt::type_list<bool(int) const> {
  126. template<typename Base>
  127. struct type: Base {
  128. bool draw(int pt) const { return entt::poly_call<0>(*this, pt); }
  129. };
  130. // ...
  131. };
  132. ```
  133. Why should a user fully define a concept if the function types are the same as
  134. the deduced ones?<br>
  135. Because, in fact, this is exactly the limitation that can be worked around by
  136. manually defining the static virtual table.
  137. When things are deduced, there is an implicit constraint.<br/>
  138. If the concept exposes a member function called `draw` with function type
  139. `void()`, a concept can be satisfied:
  140. * Either by a class that exposes a member function with the same name and the
  141. same signature.
  142. * Or through a lambda that makes use of existing member functions from the
  143. interface itself.
  144. In other words, it's not possible to make use of functions not belonging to the
  145. interface, even if they are present in the types that fulfill the concept.<br/>
  146. Similarly, it's not possible to deduce a function in the static virtual table
  147. with a function type different from that of the associated member function in
  148. the interface itself.
  149. Explicitly defining a static virtual table suppresses the deduction step and
  150. allows maximum flexibility when providing the implementation for a concept.
  151. ## Fulfill a concept
  152. The `impl` alias template of a concept is used to define how it's fulfilled:
  153. ```cpp
  154. struct Drawable: entt::type_list<> {
  155. // ...
  156. template<typename Type>
  157. using impl = entt::value_list<&Type::draw>;
  158. };
  159. ```
  160. In this case, it's stated that the `draw` method of a generic type will be
  161. enough to satisfy the requirements of the `Drawable` concept.<br/>
  162. Both member functions and free functions are supported to fulfill concepts:
  163. ```cpp
  164. template<typename Type>
  165. void print(Type &self) { self.print(); }
  166. struct Drawable: entt::type_list<void()> {
  167. // ...
  168. template<typename Type>
  169. using impl = entt::value_list<&print<Type>>;
  170. };
  171. ```
  172. Likewise, as long as the parameter types and return type support conversions to
  173. and from those of the function type referenced in the static virtual table, the
  174. actual implementation may differ in its function type since it's erased
  175. internally.<br/>
  176. Moreover, the `self` parameter isn't strictly required by the system and can be
  177. left out for free functions if not required.
  178. Refer to the inline documentation for more details.
  179. # Inheritance
  180. _Concept inheritance_ is straightforward due to how poly looks like in `EnTT`.
  181. Therefore, it's quite easy to build hierarchies of concepts if necessary.<br/>
  182. The only constraint is that all concepts in a hierarchy must belong to the same
  183. _family_, that is, they must be either all deduced or all defined.
  184. For a deduced concept, inheritance is achieved in a few steps:
  185. ```cpp
  186. struct DrawableAndErasable: entt::type_list<> {
  187. template<typename Base>
  188. struct type: typename Drawable::template type<Base> {
  189. static constexpr auto base = std::tuple_size_v<typename entt::poly_vtable<Drawable>::type>;
  190. void erase() { entt::poly_call<base + 0>(*this); }
  191. };
  192. template<typename Type>
  193. using impl = entt::value_list_cat_t<
  194. typename Drawable::impl<Type>,
  195. entt::value_list<&Type::erase>
  196. >;
  197. };
  198. ```
  199. The static virtual table is empty and must remain so.<br/>
  200. On the other hand, `type` no longer inherits from `Base` and instead forwards
  201. its template parameter to the type exposed by the _base class_. Internally, the
  202. size of the static virtual table of the base class is used as an offset for the
  203. local indexes.<br/>
  204. Finally, by means of the `value_list_cat_t` utility, the implementation consists
  205. in appending the new functions to the previous list.
  206. As for a defined concept instead, also the list of types must be extended, in a
  207. similar way to what is shown for the implementation of the above concept.<br/>
  208. To do this, it's useful to declare a function that allows to convert a _concept_
  209. into its underlying `type_list` object:
  210. ```cpp
  211. template<typename... Type>
  212. entt::type_list<Type...> as_type_list(const entt::type_list<Type...> &);
  213. ```
  214. The definition isn't strictly required, since the function will only be used
  215. through a `decltype` as it follows:
  216. ```cpp
  217. struct DrawableAndErasable: entt::type_list_cat_t<
  218. decltype(as_type_list(std::declval<Drawable>())),
  219. entt::type_list<void()>
  220. > {
  221. // ...
  222. };
  223. ```
  224. Similar to above, `type_list_cat_t` is used to concatenate the underlying static
  225. virtual table with the new function types.<br/>
  226. Everything else is the same as already shown instead.
  227. # Static polymorphism in the wild
  228. Once the _concept_ and implementation have been introduced, it will be possible
  229. to use the `poly` class template to contain instances that meet the
  230. requirements:
  231. ```cpp
  232. using drawable = entt::poly<Drawable>;
  233. struct circle {
  234. void draw() { /* ... */ }
  235. };
  236. struct square {
  237. void draw() { /* ... */ }
  238. };
  239. // ...
  240. drawable instance{circle{}};
  241. instance->draw();
  242. instance = square{};
  243. instance->draw();
  244. ```
  245. The `poly` class template offers a wide range of constructors, from the default
  246. one (which will return an uninitialized `poly` object) to the copy and move
  247. constructors, as well as the ability to create objects in-place.<br/>
  248. Among others, there is also a constructor that allows users to wrap unmanaged
  249. objects in a `poly` instance (either const or non-const ones):
  250. ```cpp
  251. circle shape;
  252. drawable instance{std::in_place_type<circle &>, shape};
  253. ```
  254. Similarly, it's possible to create non-owning copies of `poly` from an existing
  255. object:
  256. ```cpp
  257. drawable other = instance.as_ref();
  258. ```
  259. In both cases, although the interface of the `poly` object doesn't change, it
  260. won't construct any element or take care of destroying the referenced objects.
  261. Note also how the underlying concept is accessed via a call to `operator->` and
  262. not directly as `instance.draw()`.<br/>
  263. This allows users to decouple the API of the wrapper from that of the concept.
  264. Therefore, where `instance.data()` will invoke the `data` member function of the
  265. poly object, `instance->data()` will map directly to the functionality exposed
  266. by the underlying concept.
  267. # Storage size and alignment requirement
  268. Under the hood, the `poly` class template makes use of `entt::any`. Therefore,
  269. it can take advantage of the possibility of defining at compile-time the size of
  270. the storage suitable for the small buffer optimization as well as the alignment
  271. requirements:
  272. ```cpp
  273. entt::basic_poly<Drawable, sizeof(double[4]), alignof(double[4])>
  274. ```
  275. The default size is `sizeof(double[2])`, which seems like a good compromise
  276. between a buffer that is too large and one unable to hold anything larger than
  277. an integer. The alignment requirement is optional instead and by default such
  278. that it's the most stringent (the largest) for any object whose size is at most
  279. equal to the one provided.<br/>
  280. It's worth noting that providing a size of 0 (which is an accepted value in all
  281. respects) will force the system to dynamically allocate the contained objects in
  282. all cases.