Beautiful Type Erasure with C++26 Reflection

Home
Beautiful Type Erasure with C++26 Reflection
Post
Cancel
Beautiful Type Erasure with C++26 Reflection
Posted
Jul 13, 2026
Updated
Jul 14, 2026
By
Ryan Keane
20 min
read
Beautiful Type Erasure with C++26 Reflection
Contents
Beautiful Type Erasure with C++26 Reflection
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
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

// …
struct
[[
=
rjk
::
trait
]]
Container
{
auto
size
()
const
->
std
::
size_t
;
auto
empty
()
const
->
bool
;
auto
clear
()
->
void
;
};
rjk
::
duck

c
{
std
::
vector

{
1
,
2
,
3
}};
c
.
size
();
// 3
c
=
std
::
string
{
“hello”
};
// swap underlying type at runtime
c
.
size
();
// 5
c
=
std
::
map

{{
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
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 void>
and
has_fn 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

{
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
const
auto
tag
=
substitute
(
^^
has_fn
,
{
reflect_constant
(
name
),
signature
});
return
tag
;
})
|
std
::
ranges
::
to

();
}
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
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

struct
vtable_generator
{
struct
vtable
;
// …
consteval
{
std
::
vector

members
{
/* typeid, copy, move, destroy… */
};
constexpr
static
std
::
array

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

(
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

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_member
);
}
return
result
;
}
template

constexpr
static
auto
vtable_for
=
make_vtable

();
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
At its core, this is the same trick any type erasure library uses.
1
2
3
4
5
6
7
template

struct
vtable_fn_maker
{
constexpr
static
auto
erased_call
(
void
*
self
,
Args
…
args
)
->
Ret
{
auto
*
typed
=
static_cast

(
self
);
return
std
::
invoke
(
Invoker
{},
*
typed
,
std
::
forward

(
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
The reader accustomed to C++17 might be familiar with this common utility:
1
2
3
4
template

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

struct
candidate_wrapper
{
constexpr
decltype
(
auto
)
operator
()(
Self
self
,
Args
…
args
)
const
{
return
std
::
invoke
(
&
[
:
Member
:
],
std
::
forward

(
self
),
std
::
forward

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

class
vtable_function
;
template

class
vtable_function

Ret
>
{
public:
constexpr
vtable_function
(
duck
&
owner
)
:
m_owner
(
owner
)
{}
constexpr
auto
operator
()(
Args
…
args
)
->
Ret
{
return
m_owner
->
get_vtable
()
->
[
:
VtableMember
:
](
m_owner
->
get_underlying
(),
std
::
forward

(
args
)…
);
}
private
:
duck
*
m_owner
;
};
// generated (somewhere) by define_aggregate
struct
vtable_wrapper

{
vtable_function

int
>
foo
;
vtable_function

double
>
bar
;
};
class
duck

:
public
vtable_wrapper

{
private:
auto
get_vtable
()
const
->
const
vtable
*
{
…
}
auto
get_underlying
()
->
void
*
{
…
}
public
:
template

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

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

// remember the has_fn int> from before?
class
duck_base
{
private:
template

class
vtable_function
;
template

class
vtable_function

Ret
>
{
public:
// no more duck back-pointer
constexpr
auto
operator
()(
Args
…
args
)
->
Ret
;
};
template

struct
vtable_function_wrapper
;
consteval
{
constexpr
std
::
array

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

(
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

class
duck_base
{
private:
// everything from before
public:
struct
vtable_wrapper
:
vtable_function_wrapper

…
{};
};
template

using
make_duck_base_t
=
[
:
/* convert traits to tags using members_to_tags */
:
];
template

class
duck
:
public
make_duck_base_t
<
duck
>::
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

template

constexpr
auto
duck_base
::
vtable_function

Ret
>
::
operator
()(
Args
…
args
)
->
Ret
{
// First, we cast to the pointer-interconvertible wrapper class
auto
*
wrapper_ptr
=
reinterpret_cast
<
vtable_function_wrapper
*>
(
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

(
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
)…
);
}
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
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
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
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
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

{
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

{
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
Without getting too in the weeds of
duck
’s API (you can read more about this in the
docs
), 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
will inline
importantFunc
like in the example above.
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

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

constexpr
decltype
(
auto
)
call
(
auto
*
underlying
,
Args
&&
…
args
)
const
{
if
constexpr
(
is_inlined_function
(
Member
))
{
return
m_inlined
.[
:
Member
:
](
underlying
,
std
::
forward

(
args
)…);
}
else
{
return
m_vtable
->
[
:
Member
:
](
underlying
,
std
::
forward

(
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
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
to dig into many of its other features and their (also interesting) implementation details. You can also
try the library out directly
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.
This post is licensed under
CC BY 4.0
by the author.
Share
Recently Updated
Beautiful Type Erasure with C++26 Reflection
Simulating Infinity in Conway’s Game of Life with Modern C++
Contents
Simulating Infinity in Conway’s Game of Life with Modern C++
–
©
2026
Ryan Keane
.
Some rights reserved.
Using the
Chirpy
theme for
Jekyll
.