Cached at:
07/14/26, 01:16 PM
# Beautiful Type Erasure with C++26 Reflection
Source: [https://ryanjk5.github.io/posts/rjk-duck/](https://ryanjk5.github.io/posts/rjk-duck/)
If you’ve ever tried using type erasure for something more complicated than`std::any`or`std::function`, you’ve either written 100\+ lines of easy\-to\-mess\-up code or reached for a boilerplate\-heavy library like`Boost\.TypeErasure`or`Folly\.Poly`\.[`rjk::duck`](https://github.com/RyanJK5/rjk-duck)uses the magic that is C\+\+26 reflection to remove these pain points while preserving all of the customization and performance\.
Consider the following basic example:
`1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \#include <rjk/duck\.hpp\> // \.\.\. struct \[\[=rjk::trait\]\] Container \{ auto size\(\) const \-\> std::size\_t; auto empty\(\) const \-\> bool; auto clear\(\) \-\> void; \}; rjk::duck<Container\> c\{std::vector<int\>\{1, 2, 3\}\}; c\.size\(\); // 3 c = std::string\{"hello"\}; // swap underlying type at runtime c\.size\(\); // 5 c = std::map<int, int\>\{\{1, 2\}, \{3, 4\}\}; c\.empty\(\); // false c\.clear\(\); c\.empty\(\); // true`
Simply declare the interface once and let your already\-written definitions handle the rest\.
The library itself is a single header include\. It offers owning and non\-owning semantics, operators, interface composition, adapters for existing interfaces, extension methods for third\-party types, and much more\.
We’re talking about the bleeding edge here, so right now support is only available for gcc with`\-std=c\+\+26 \-freflection`\. duck uses reflection in some unique ways that go beyond the simple enum\-to\-string or JSON serialization examples you may have seen, so we’ll spend the rest of the article demystifying the tricks that make this library possible\.
In particular, we’ll walk through tag generation, vtable codegen, overload resolution, and the interconvertibility trick that keeps`duck`small\.
## A Brief Intro to C\+\+26 Reflection[https://ryanjk5.github.io/posts/rjk-duck/#a-brief-intro-to-c26-reflection](https://ryanjk5.github.io/posts/rjk-duck/#a-brief-intro-to-c26-reflection)
You may have noticed the strange bit of syntax from the above example,`\[\[=rjk::trait\]\]`\. This is a C\+\+26**annotation**, which can be applied to a struct similarly to an attribute\. The actual definition for`trait`is simply:
`1 constexpr inline struct\{\} trait\{\};`
We can check if a type has the`trait`annotation like this:
`1 2 3 4 5 return std::ranges::any\_of\(annotations\_of\(^^MyType\), \[\]\(std::meta::info annotation\) \{ return type\_of\(annotation\) == type\_of\(^^trait\); \} \);`
The`^^`operator produces a reflection of*something*\. In this case, we are reflecting both a type \(`MyType`\) and a variable \(`trait`\)\. Reflections all have the`std::meta::info`type and can be queried with a variety of meta\-functions, like`annotations\_of`and`type\_of`\.
One of the first steps in duck’s process is interpreting the members of a trait and converting them to a**tag**, which is a format that duck uses internally\. So for this trait:
`1 2 3 4 struct \[\[=rjk::trait\]\] MyTrait \{ auto foo\(\) \-\> void; auto bar\(\) const \-\> int; \};`
Our goal is to generate a`has\_fn<"foo", auto\(\) \-\> void\>`and`has\_fn<"bar", auto\(\) const \-\> int\>`tag\. We can implement this pretty easily by inspecting the members of`MyTrait`and transforming them, like so:
`1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 consteval auto members\_to\_tags\(std::meta::info trait\) \-\> std::vector<std::meta::info\> \{ const auto ctx = std::meta::access\_context::unprivileged\(\); // Only check public members return members\_of\(trait, ctx\) // Take all members of the trait \| std::views::filter\(std::meta::is\_user\_declared\) // Exclude constructors, etc\. \| std::views::filter\(std::meta::is\_function\) // Exclude data members \| std::views::filter\(std::meta::has\_identifier\) // Exclude operator functions, etc\. \| std::views::transform\(\[\]\(std::meta::info member\) \{ // identifier\_of returns the name as a string\_view\. fixed\_string is a // custom structural type that can be used as a template argument\. const fixed\_string name\{identifier\_of\(member\)\}; const auto signature = type\_of\(member\); // Returns a function type // Create has\_fn<name, signature\> const auto tag = substitute\(^^has\_fn, \{reflect\_constant\(name\), signature\}\); return tag; \}\) \| std::ranges::to<std::vector\>\(\); \}`
The actual implementation has to handle a variety of other aspects to this, such as iterating base classes for traits, handling`const`traits, operators, and more\. But the core transformation is as simple as it looks\.
## Generating a vtable[https://ryanjk5.github.io/posts/rjk-duck/#generating-a-vtable](https://ryanjk5.github.io/posts/rjk-duck/#generating-a-vtable)
The mechanism for code generation we got in C\+\+26 is limited, but still very powerful\. Let’s walk through each component of how the vtable is generated, starting with creating the struct:
`1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 template <is\_trait\.\.\. Traits\> struct vtable\_generator \{ struct vtable; // \.\.\. consteval \{ std::vector<std::meta::info\> members\{ /\* typeid, copy, move, destroy\.\.\. \*/ \}; constexpr static std::array<std::meta::info, sizeof\.\.\.\(Traits\)\> traits\{^^Traits\.\.\.\}; template for \(constexpr auto trait : traits\) \{ for \(const auto tag : members\_to\_tags\(trait\)\) \{ const auto args = template\_arguments\_of\(tag\); const auto name = extract<fixed\_string\>\(args\[0\]\); // Remove cvref qualifiers from the function const auto func\_type = remove\_fn\_qualifiers\(args\[1\]\); const auto signature = add\_pointer\(prepend\_arg\(func\_type, ^^void\*\)\); const auto member = data\_member\_spec\(signature, \{\.name = name\}\); members\.push\_back\(member\); \} \} define\_aggregate\(^^vtable, members\); \} \};`
The`consteval`block is a new feature and is the only context in which we can currently generate code, using`define\_aggregate`\.`template for`is the new syntax for an expansion statement, which lets us iterate over parameter packs without having to rely on fold expressions\.
Other than that, the code is pretty straightforward\. We simply collect all of the traits, then collect all of their respective tags, and generate function pointers for each member function in the traits\.
One simplification worth acknowledging is that this approach can’t handle overloads, since you can’t have two members with the same name\. The actual code will assign names like`slot\_0`,`slot\_1`, etc\. and re\-derive them later\. We’re also just using plain`void\*`here, but in reality we need to also make this`const void\*`based on the function’s qualifier\.
Creating a static vtable for a type is likewise not too complicated:
`1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // still inside vtable\_generator template <typename T\> consteval static auto make\_vtable\(\) \-\> vtable \{ constexpr static auto ctx = std::meta::access\_context::unprivileged\(\); constexpr static auto slots = define\_static\_array\( nonstatic\_data\_members\_of\(^^vtable, ctx\) \| std::views::drop\(/\* copy, move, etc\. \*/\) \); vtable result\{\}; template for \(constexpr auto index : std::views::indices\(slots\.size\(\)\)\) \{ constexpr auto T\_member = \*std::ranges::find\_if\( members\_of\(^^T, ctx\), \[\]\(std::meta::info member\) \{ return is\_function\(member\) && has\_identifier\(member\) && identifier\_of\(member\) == identifier\_of\(slots\[index\]\) && type\_of\(member\) == type\_of\(slots\[index\]\); \} \); result\.\[: slots\[index\] :\] = convert\_to\_vtable\_func<T\>\(T\_member\); \} return result; \} template <typename T\> constexpr static auto vtable\_for = make\_vtable<T\>\(\);`
Real overload resolution for finding`T\_member`is significantly more involved \(see below\)\. We’re matching by exact signature here for clarity\.
The splice operator \(`\[: expr :\]`\) is another new feature, and it’s what brings your code from the land of reflection back into reality\. Here, we use it to actually assign to each of the function pointers that were added to the generated vtable\.`std::views::indices`is a nice library utility we got as well, which is simply equivalent to`std::views::iota\(0, upper\_bound\)`\.
The one loose thread remaining is`convert\_to\_vtable\_func`\. Somehow, we need to turn a`T\_member`into an actual`auto\(\*\)\(void\*, Args\.\.\.\) \-\> Ret`that we can store and call through\. That conversion is where duck’s type erasure actually happens, so it’s worth taking apart properly\.
### From Slot to Call[https://ryanjk5.github.io/posts/rjk-duck/#from-slot-to-call](https://ryanjk5.github.io/posts/rjk-duck/#from-slot-to-call)
At its core, this is the same trick any type erasure library uses\.
`1 2 3 4 5 6 7 template <typename T, typename Invoker, typename Ret, typename\.\.\. Args\> struct vtable\_fn\_maker \{ constexpr static auto erased\_call\(void\* self, Args\.\.\. args\) \-\> Ret \{ auto\* typed = static\_cast<T\*\>\(self\); return std::invoke\(Invoker\{\}, \*typed, std::forward<Args\>\(args\)\.\.\.\); \} \};`
`convert\_to\_vtable\_func`\(roughly\) will end up generating a pointer to this`erased\_call`function, which ultimately gets stored in the vtable\.
The`Invoker`here is suspicious, and doesn’t match the`T\_member`we saw from the previous section\. Previously, we just looked for an exact function match, but actual overload resolution can’t be that simple\. As it turns out, duck doesn’t try to reimplement it by hand\.
### Making the Call[https://ryanjk5.github.io/posts/rjk-duck/#making-the-call](https://ryanjk5.github.io/posts/rjk-duck/#making-the-call)
The reader accustomed to C\+\+17 might be familiar with this common utility:
`1 2 3 4 template <typename\.\.\. Callables\> struct overload\_set : Callables\.\.\. \{ using Callables::operator\(\)\.\.\.; \};`
Rather than trying to manually reproduce C\+\+ overload resolution, we instead generate a callable`overload\_set`and simply let the language do the work\. The actual mechanism relies on two parts\. First,`candidate\_wrapper`:
`1 2 3 4 5 6 template <std::meta::info Member, typename Self, typename\.\.\. Args\> struct candidate\_wrapper \{ constexpr decltype\(auto\) operator\(\)\(Self self, Args\.\.\. args\) const \{ return std::invoke\(&\[:Member:\], std::forward<Self\>\(self\), std::forward<Args\>\(args\)\.\.\.\); \} \};`
This is a simple wrapper class that takes in some member function from`Self`, splices it \(`&\[:Member:\]`\) to obtain a member function pointer, and then invokes it with the given type and arguments\. The key detail is that this turns any`myObj\.foo\(args\.\.\.\)`call into a`myWrapper\(myObj, args\.\.\.\)`call, which can then get substituted into`overload\_set`like so:
`1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 consteval auto make\_set\(std::meta::info type, std::string\_view identifier\) \-\> std::meta::info \{ const auto members = members\_of\(type, ctx\); // ctx is still a std::meta::access\_context const auto candidates = members \| std::views::filter\(std::meta::is\_function\) \| std::views::filter\(std::meta::has\_identifier\) \| std::views::filter\(\[=\]\(std::meta::info member\) \{ return identifier\_of\(member\) == identifier; \}\) \| std::views::transform\(\[=\]\(std::meta::info member\) \{ // Check the qualifiers and create a const T&, T&&, etc\. const auto self\_type = self\_type\_of\(member\); const auto params = parameters\_of\(member\); const std::array member\_and\_self\{reflect\_constant\(member\), self\_type\}; const auto args = std::views::concat\( member\_and\_self, params \| std::views::transform\(std::meta::type\_of\) \); return substitute\(^^candidate\_wrapper, args\); \}\); return substitute\(^^overload\_set, candidates\); \}`
There’s a couple of subtleties that come with getting this right:`self\_type\_of`will query the qualifiers of the member function and determine the qualifiers to apply to the`Self`template parameter;`std::views::concat`is a new utility that lets us cheaply concatenate two ranges, without having to materialize them into an actual`std::vector`;`params`gets a small transformation to convert the parameter reflections into type reflections\. But the payoff is that overload resolution now works cleanly:
`1 2 3 4 5 6 7 8 9 struct \[\[=rjk::trait\]\] MyTrait \{ auto foo\(int\) \-\> int; \}; struct MyStruct \{ auto foo\(double\) \-\> int; auto foo\(int\) const \-\> int; \};`
`make\_set\(^^MyStruct, "foo"\)`will collect both of the overloads, and then we will attempt to call them on a`MyStruct&`and`MyStruct&&`with an`int`as an argument\. This will match the`auto foo\(int\) const \-\> int`signature\. So even though`MyStruct`doesn’t literally define a mutable`foo`member function that accepts an`int`, it can still match`MyTrait`\.
The`T\_member`search from`make\_vtable`was the simplification\. In reality, each vtable slot is filled with a`vtable\_fn\_maker`whose`Invoker`is our compile\-time generated`overload\_set`\. Resolution happens right inside of`erased\_call`, handled entirely by the compiler\.
All of this gets us a fully populated`vtable\_for<T\>`, but it doesn’t answer the question of how we can get the clean`myDuck\.foo\(5\)`syntax we’re looking for\. Somehow,`duck`needs to grow a callable member, one for each trait member function\.
## Growing an Interface[https://ryanjk5.github.io/posts/rjk-duck/#growing-an-interface](https://ryanjk5.github.io/posts/rjk-duck/#growing-an-interface)
Reflection doesn’t let us inject member functions yet, so the best we can do is starting with a simple wrapper type which our`duck`class can then inherit from\. So for the following simple trait:
`1 2 3 4 struct \[\[=rjk::trait\]\] MyTrait \{ auto foo\(int\) \-\> int; auto bar\(\) \-\> double; \};`
We can fill in`duck`like this \(I’m skipping some pieces of this for brevity, e\.g\. we need to forward declare`duck`\):
`1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 // Wraps a raw call to the vtable from before cleanly \(no void\* exposed\) template <std::meta::info VtableMember, typename Func\> class vtable\_function; template <std::meta::info VtableMember, typename Ret, typename\.\.\. Args\> class vtable\_function<VtableMember, auto\(Args\.\.\.\) \-\> Ret\> \{ public: constexpr vtable\_function\(duck<MyTrait\>& owner\) : m\_owner\(owner\) \{\} constexpr auto operator\(\)\(Args\.\.\. args\) \-\> Ret \{ return m\_owner\-\>get\_vtable\(\)\-\>\[: VtableMember :\]\( m\_owner\-\>get\_underlying\(\), std::forward<Args\>\(args\)\.\.\. \); \} private: duck<MyTrait\>\* m\_owner; \}; // generated \(somewhere\) by define\_aggregate struct vtable\_wrapper<MyTrait\> \{ vtable\_function</\* vtable member \*/, auto\(int\) \-\> int \> foo; vtable\_function</\* vtable member \*/, auto\(\) \-\> double\> bar; \}; class duck<MyTrait\> : public vtable\_wrapper<MyTrait\> \{ private: auto get\_vtable\(\) const \-\> const vtable<MyTrait\>\* \{ \.\.\. \} auto get\_underlying\(\) \-\> void\* \{ \.\.\. \} public: template <std::meta::info VtableMember, typename Func\> friend class vtable\_function; // \.\.\. \};`
`duck`inherits from`vtable\_wrapper`and thereby takes in all of the`vtable\_function`callable objects with the appropriate names, so we get the`myDuck\.bar\(\)`syntax we want\. We also have to go through each of the vtable functions in the constructor and set their`owner`back\-pointer to look at the enclosing`duck`\.
But this approach has a problem, and it’s a big one\. That`owner`back pointer is not cheap:`sizeof\(duck\)`now scales linearly with the number of traits we use, since each one needs a back pointer\. Combining that with the vtable pointer and the data pointer that`duck`already stores, this could make what should be a lean type incredibly large\.
Realistically, there’s no reason these`vtable\_function`objects*shouldn’t*know about`duck`\. After all, they’re each simulating member functions and aren’t used in any other context, so there should be some way for them to recover the`this`pointer without having to explicitly store it\.
### Pointer\-Interconvertibility[https://ryanjk5.github.io/posts/rjk-duck/#pointer-interconvertibility](https://ryanjk5.github.io/posts/rjk-duck/#pointer-interconvertibility)
To understand the solution, we must first take a detour to an oft\-forgot feature of the C\+\+ standard\. Consider the following example:
`1 2 3 4 5 6 7 8 9 struct SomeType \{\}; struct SomeStruct \{ SomeType t; \}; SomeStruct s\{\}; auto\* ptr = reinterpret\_cast<SomeStruct\*\>\(&s\.t\); assert\(&s == ptr\);`
Even though`SomeType`and`SomeStruct`are completely distinct objects with no defined conversions, it is legal to`reinterpret\_cast`between a pointer to`SomeStruct::t`and`SomeStruct`\. Intuitively, this makes sense: the first data member of`SomeStruct`will naturally occupy the same location in memory as`SomeStruct`itself\. The two types are therefore considered**pointer\-interconvertible**\.
But this comes with some strict limitations, namely that the types involved must be**standard layout**\. I don’t want to dwell too much on the standardese here, but just know that this only works if`SomeStruct`is a very simple type, and`SomeType`is as well\. This works perfectly for what we’re trying to accomplish\.
### The vanishing`this`pointer[https://ryanjk5.github.io/posts/rjk-duck/#the-vanishing-this-pointer](https://ryanjk5.github.io/posts/rjk-duck/#the-vanishing-this-pointer)
Instead of putting all of our`vtable\_function`objects in a single`vtable\_wrapper`table, let’s try splitting them into several small`vtable\_function\_wrapper`s instead\. I’ll be a bit more explicit about the code now as well, so we’ll do all of this inside a dependent context\.
`1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 template <typename Derived, is\_tag\.\.\. Tags\> // remember the has\_fn<"foo", auto\(\) \-\> int\> from before? class duck\_base \{ private: template <std::meta::info VtableMember, is\_tag Tag, typename Func\> class vtable\_function; template <std::meta::info VtableMember, is\_tag Tag, typename Ret, typename\.\.\. Args\> class vtable\_function<VtableMember, Tag, auto\(Args\.\.\.\) \-\> Ret\> \{ public: // no more duck back\-pointer constexpr auto operator\(\)\(Args\.\.\. args\) \-\> Ret; \}; template <is\_tag Tag\> struct vtable\_function\_wrapper; consteval \{ constexpr std::array<std::meta::info, sizeof\.\.\.\(Tags\)\> tags\{^^Tags\.\.\.\}; template for \(constexpr auto tag : tags\) \{ const auto args = template\_arguments\_of\(tag\); // find the correct member in the static\_vtable from the previous section const auto vtable\_member = \.\.\.; const auto func\_type = remove\_fn\_qualifiers\(args\[1\]\); const auto function\_type = substitute\(^^vtable\_function, \{ reflect\_constant\(vtable\_member\), tag, func\_type \}\); const auto wrapper = substitute\(^^vtable\_function\_wrapper, \{tag\}\); const std::string\_view identifier\{extract<fixed\_string\>\(args\[0\]\)\}; const auto member\_spec = data\_member\_spec\(function\_type, \{ \.name = identifier, \.no\_unique\_address = true \}\); define\_aggregate\(wrapper, \{ member\_spec \}\); // define an aggregate for each tag \} \} \};`
This may look complicated, but all we’re doing is defining a`vtable\_function\_wrapper`for each tag with a single`vtable\_function`member\. The members get their appropriate names \(say`foo`or`bar`\), and all can get marked as`no\_unique\_address`, since`vtable\_function`is now an empty class type\.
Once we have all of these, we can assemble the entire vtable through inheritance:
`1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <typename Derived, is\_tag\.\.\. Tags\> class duck\_base \{ private: // everything from before public: struct vtable\_wrapper : vtable\_function\_wrapper<Tags\>\.\.\. \{\}; \}; template <typename Derived\> using make\_duck\_base\_t = \[: /\* convert traits to tags using members\_to\_tags \*/ :\]; template <is\_trait\.\.\. Traits\> class duck : public make\_duck\_base\_t<duck<Traits\.\.\.\>\>::vtable\_wrapper \{ // \.\.\. \};`
All of this arduous setup gets us close, but we haven’t yet written the implementation for our new`vtable\_function`’s call operator\. This is where we use the special pointer\-interconvertibility trick\.
`1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 template <typename Derived, is\_tag\.\.\. Tags\> template <std::meta::info VtableMember, is\_tag Tag, typename Ret, typename\.\.\. Args\> constexpr auto duck\_base<Derived, Tags\.\.\.\>::vtable\_function<VtableMember, Tag, auto\(Args\.\.\.\) \-\> Ret\> ::operator\(\)\(Args\.\.\. args\) \-\> Ret \{ // First, we cast to the pointer\-interconvertible wrapper class auto\* wrapper\_ptr = reinterpret\_cast<vtable\_function\_wrapper<Tag\>\*\>\(this\); // Then, since duck \(Derived\) inherits from vtable\_wrapper, which inherits // from vtable\_function\_wrapper, we can static\_cast all the way down auto\* owner = static\_cast<Derived\*\>\(wrapper\_ptr\); // Finally, we can do the actual vtable call return owner\-\>get\_vtable\(\)\-\>\[: VtableMember :\]\( owner\-\>get\_underlying\(\), // void\* as first argument std::forward<Args\>\(args\)\.\.\. \); \}`
With that,`myDuck\.foo\(5\)`resolves all the way through to the real, underlying call, and`sizeof\(duck\)`stops caring about how many functions your traits declare\. The expressive syntax reflection bought us adds no runtime overhead to traditional vtable dispatch, but makes using type erasure that much more ergonomic\.
### `constexpr`ducks[https://ryanjk5.github.io/posts/rjk-duck/#constexpr-ducks](https://ryanjk5.github.io/posts/rjk-duck/#constexpr-ducks)
The astute reader may have noticed another neat thing about all of these code examples, which is that every function has been marked`constexpr`\. Unfortunately, using`duck`is not yet well\-defined at compile\-time, but it comes close\.[P2738](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2738r1.pdf)allows casting back and forth from a`void\*`, so the type\-erased backend actually can work at compile\-time\.
The problem is the interconvertibility trick we just discussed\. There’s no`reinterpret\_cast`allowed at compile\-time, and`void\*`can only be cast back to the exact type it was originally, not one that it’s convertible to\.
As an aside, gcc\-trunk actually currently does allow dancing through a`void\*`at compile\-time, so if you want to try out`duck`at compile time, you can \(while it lasts\!\)\.
## Squeezing Out Performance[https://ryanjk5.github.io/posts/rjk-duck/#squeezing-out-performance](https://ryanjk5.github.io/posts/rjk-duck/#squeezing-out-performance)
The code we’ve written is already about as performant as traditional vtable dispatch gets\. However, we can take a page out of the[Dyno](https://github.com/ldionne/dyno)library and offer some ways to make it even faster\. The one I will discuss is storing function pointers inline instead of in a vtable\.
So consider`duck`from earlier:
`1 2 3 4 5 6 7 class duck<Traits\.\.\.\> \{ public: // \.\.\. private: void\* m\_underlying; const vtable\* m\_vtable; \};`
Calling a function on`duck`requires first loading`m\_vtable`, which is stored in static memory and may be cold\. After that, we get to call the erased function pointer, which does the actual virtual dispatch\.
But suppose we want to trade that potentially\-cold vtable load for a few extra bytes on`duck`\. You might want something like this:
`1 2 3 4 5 6 7 8 class duck<Traits\.\.\.\> \{ public: // \.\.\. private: void\* m\_underlying; const vtable\* m\_vtable; // still need for copy, move, destroy auto \(\*importantFunc\)\(int, int\) \-\> int; \};`
Granted,`duck`is now 8 bytes larger, but you can call`importantFunc`directly without the vtable indirection\.`duck`offers first\-class support for this feature, and it’s implemented with \(you guessed it\!\) reflection\.
### The`perf\_options`struct[https://ryanjk5.github.io/posts/rjk-duck/#the-perf_options-struct](https://ryanjk5.github.io/posts/rjk-duck/#the-perf_options-struct)
Without getting too in the weeds of`duck`’s API \(you can read more about this in the[docs](https://github.com/RyanJK5/rjk-duck/blob/master/docs/ducks/05_performance_tuning.md)\), you can define a special trait with performance settings that are applied to a`duck`:
`1 2 3 4 5 6 7 8 9 struct \[\[=rjk::trait\]\] MyTrait \{ auto importantFunc\(int, int\) \-\> int; \}; struct \[\[=rjk::perf\_options\]\] MyPerfOptions \{ struct inlined\_functions \{ auto importantFunc\(int, int\) \-\> int; \}; \};`
And now using a`rjk::duck<MyTrait, MyPerfOptions\>`will inline`importantFunc`like in the example above\.
### The`vtable\_caller`Wrapper[https://ryanjk5.github.io/posts/rjk-duck/#the-vtable_caller-wrapper](https://ryanjk5.github.io/posts/rjk-duck/#the-vtable_caller-wrapper)
Instead of calling vtable functions directly using`get\_vtable\(\)`, we will wrap our calls in a struct called`vtable\_caller`\. Now we can add inlined functions like so:
`1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 template <typename VtableGenerator\> class vtable\_caller \{ private: struct inlined\_functions; using options = \[: /\* find perf\_options trait, or fall back to default \*/ :\]; consteval \{ const auto tags = members\_to\_tags\(^^typename options::inlined\_functions\); const auto members = VtableGenerator::tags // has\_fn, etc\. \| std::views::filter\(\[&tags\]\(std::meta::info tag\) \{ return std::ranges::contains\(tags, tag\); \}\) \| std::views::transform\(\[\]\(std::meta::info tag\) \{ return VtableGenerator::make\_vtable\_member\(tag\); \}\); define\_aggregate\(^^inlined\_functions, members\); \} public: constexpr explicit vtable\_caller\(const auto\* v\) : m\_inlined\(inline\_from\_vtable\(v\)\) , m\_vtable\(v\) \{ \} template <std::meta::info Member, bool Noexcept, typename\.\.\. Args\> constexpr decltype\(auto\) call\(auto\* underlying, Args&&\.\.\. args\) const \{ if constexpr \(is\_inlined\_function\(Member\)\) \{ return m\_inlined\.\[:Member:\]\(underlying, std::forward<Args\>\(args\)\.\.\.\); \} else \{ return m\_vtable\-\>\[:Member:\]\(underlying, std::forward<Args\>\(args\)\.\.\.\); \} \} private: \[\[no\_unique\_address\]\] inlined\_functions m\_inlined; const VtableGenerator::vtable\* m\_vtable; \};`
Put simply, we \(A\) find the perf\_options struct itself, \(B\) define inlined\_functions with the appropriate members from the vtable \(the`consteval`block\), and \(C\) fill in these inlined slots at construction\. I’ve abridged the mechanics of this for brevity, largely because the full implementation is fairly similar to what we did before with`vtable`\.
As a result of the changes here, we have to thread the new`call`through the rest of the codebase now, and wire in the inlined`Member`to vtable\_function ahead of time, but on the whole it’s not too complicated\.
Again, this isn’t free, and can increase the size of the`duck\_view`type to the point where it may no longer be cheap\-to\-copy\. Measuring these tradeoffs is always important, but reflection makes them a lot easier to iterate upon than in the past\.
## Conclusion[https://ryanjk5.github.io/posts/rjk-duck/#conclusion](https://ryanjk5.github.io/posts/rjk-duck/#conclusion)
Step back from all the tricks, and a pattern runs through all of them: reflection is not just for cutting down on typing\.`rjk::duck`is a working demonstration that reflection can replace hand\-written machinery with code that’s shorter, safer, and tunable in ways a hand\-rolled version wouldn’t have been\.
There’s a lot more code in`duck`than what I’ve shown today, so please check out the[repo](https://github.com/RyanJK5/rjk-duck)to dig into many of its other features and their \(also interesting\) implementation details\. You can also[try the library out directly](https://godbolt.org/z/91dj5jeGW)on Compiler Explorer\.
As`duck`matures, I’m hoping to add even more customization to the library \(`variant`\-style backend, allocator support, multi\-trait narrowing…\), and hopefully patch some of its limitations alongside new C\+\+ standards\. Any help you can provide, be it issues, PRs, or general feedback, would be greatly appreciated\.