All types of Polymorphism in C++
Recently I came across row polymorphism in compiled languages via typeclasses. And while trying to find out why it’s called that, I found a StackOverflow thread which mentioned a type of polymorphism I haven’t heard of. So I looked up all the different types and I’m pretty sure the 5 found on Wikipedia can all be replicated in one language.
Ad-hoc polymorphism🔗
This one is already a built-in fully supported feature of many languages. Choosing the implementation of a function based on the argument’s types is overloading.
Parametric polymorphism🔗
This one is also built-in thanks to templates. C++ also supports a limited form of values as types, since templates can take values, and the type then contains those values, for example std::array<20, int>.
Subtype (inclusion) polymorphism🔗
I pondered skipping some of these, but there’s some things to be said, even for the language supported ones. For example that C++ does not allow this type of polymorphism for non-reference (or pointer) values and vice-versa cannot talk about pointers that cannot be polymorphic.
struct A { };
struct B : A { };
B b;
// fully becomes A
// methods are not overriden and information is lost
A a = b;
// this always works for pointers
A *a_ptr = &b;
Contrasting this with Ada, where these concepts are separate. Given:
type A is tagged record
null;
end record;
type B is new A with record
null;
end record;
B_Val : aliased B;
We cannot assign it to its superclass:
A_Val : A := B_Val;>>> error: expected type "A"
>>> error: found type "B"
However we can’t assign their accesses (what Ada calls pointers) either:
A_Ptr : access A := B_Val'Access;>>> error: expected an access type with designated type "A"
>>> error: found an access type with designated type "B"
The ability to subtype is accessed1 with another feature called 'Class. Which refers to everything in the class hierarchy of the given type and can be used for both values and pointers:
A_Val : A'Class := B_Val;
A_Ptr : access A'Class := B_Val'Access;
If we provide the following primitive operations we can verify how our polymorphism applies:
function Me(Val: A) return String is ("I Am A");
function Me(Val: B) return String is ("I Am B");
This code:
declare
True_A : A;
A_Val : A'Class := B_Val;
A_Ptr : access A'Class := B_Val'Access;
begin
Put_Line(True_A.Me & ASCII.LF &
A_Val.Me & ASCII.LF &
A_Ptr.Me);
end;
Will print:
I Am A
I Am B
I Am B
Showing that inheritance subtyping works regardless of if we use pointers or values.
Row polymorphism🔗
This allows us to define a function that only works if a structure contains a specific slot.
Thanks to templates this trivial use should already be enough to be called row polymorphic:
template <typename T>
void increment_foo(T &item) {
++item.foo;
}
Some form of duck typing is inherent in C++’s template system since I can insert any type at all and only afterwards check if the code template results in valid code. So if a type fits in the function it can be used.
Completely unconstrained templates like this can lead to quite gnarly errors. If we call this with a structure containing std::string foo we are informed that increment_foo(b) requires operator++ due to the line ++item.foo, this is quite fine but it’d be nicer to get the exact requirement reported for increment_foo. So we can add concepts to declare what we actually need:
template <typename T>
concept has_incrementable_foo = requires (T t) { ++t.foo; };
template <typename T>
void print_foo(T &item)
requires (has_incrementable_foo<T>)
{
++item.foo;
}In substitution of 'template<class T> void print_foo(T&) requires has_incrementable_foo<T> [with T = main()::B]':
note: the required expression '++ t.foo' is invalid
5 | concept has_incrementable_foo = requires (T t) { ++t.foo; };
| ^~~~~~~
In this simple case the result will be more or less the same, however you can see that separating out the actual requirements could lead to very understandable minimal examples for someone trying to use the function. And you can also be sure that what you provide as the example expression is what gets shown to the user rather than some random piece of code from your implementation that might change in the future.
I’ve had a few words with my friend about this, and yes it’s not that useful or different to declare these, but it at least shows the ability to describe with purpose explicitly what I expect.
It also led me to the conclusion that in the primordial soup of types, where generics and templates are born, C++ occupies a sort of “dynamic typing” position as opposed to traits/typeclasses. The difference being whether you statically declare properties of your types and the error is “Map.insert expects T to be Ord and Eq, but String isn’t” or if the code just runs anyways with whatever we put in and we only get a “runtime error in compile-time” once some built-in operation fails to work on the typeless value (in this case a type) we provided.
Polytypism🔗
We’re finally getting to the cool new stuff, as far as I could find Polytypism is a rather specific concept largely derived from how Haskell’s Generic typeclass works.
The idea is that we can deconstruct almost any type in the language into some new data structure, think converting structs to a heterogenous map from symbols to values or the like.
I’m at the very edge of barely understanding how to use this, so the explanation will be somewhat lacking. It’ll help me to understand at least, and hopefully I don’t say anything strictly wrong.
The cool part is that this description exists on the type level, simplifying a bit2 we have the constructors :+: for sum types, :*: for product types, and K1 for the fields.
Given something like:
data A = B | C Int Float
We could describe it as Unit :+: Unit :*: Int :*: Float, there’s a lot of extra metadata and other details in the GHC.Generics version but this is the main idea.
So now we can write some pseudocode:
instance (Show a, Show b) => Show (a :+: b) where _
instance (Show a, Show b) => Show (a :*: b) where _
...
This should sort of say that if the subfields of a structure can be printed the structure itself can also be printed. So I could take any type, convert it into the Genericised type Representation, which generates an implementation for the functions I wish to use, and then use to and from to convert between my struct and that representation.
I mentioned that a decent way this could be done could be a coercion from any struct to something like a Map<&str, &dyn SomeProperty>. However that would be slightly weaker as the Generic representation is fully statically typed, and I could implement instances for a :*: Int where I have full knowledge that the right argument is of type Int, without needing to reduce to some common type3.
Let’s try to replicate this in C++ using our new amazing reflection features. The equivalent could be just to convert the struct to a tuple, which incidentally is one of the examples in the proposal, although sadly:
(Note: Range splicers of the form [: … members :] were discussed in early proposals, but were omitted from C++26).
So that example, can’t be used. However, I figured that since working on an arbitrary tuple would require some form of either template recursion or reflection we might as well just use one of those directly.
template <typename T>
void takes_any_struct(const T& t) {
constexpr static auto members =
define_static_array
(nonstatic_data_members_of
(^^T,
std::meta::access_context::current()));
template for (constexpr auto member : members) {
std::println("{}: {}", identifier_of(member), t.[:member:]);
}
};
This is an example (adapted from a StackOverflow question) of something that can take any type, generate some code based on its structure, and has access to the concrete types inside. It only supports :*: since C++ doesn’t have built-in sum types, although of course we could handle std::variant in a special way, etc.
If we call it we see that it works:
struct A {
int x;
std::string b = "foo";
};
int main () {
A a;
takes_any_struct(a);
}x: 0
b: foo
I might be wrong but I think that what can be done with compile-time reflection is a superset of polytypism.
Rank polymorphism🔗
The last type I found is Rank polymorphism, which … I thought I knew more or less what is meant when that term is used, though I’m not so sure4.
It should mean that I can declare a function which applies to an array of any shape and size, although if we define a Vec [Dim] Int then we can use some form of fmap to get that behaviour. Vec [] Int plays the role of the basecase and contains a single value of that type.
Sidenote: This is also the case Common Lisp where (make-array nil) is completely valid, and you can access the contained cell using (aref v) without any indices.
This allows us to use a sort of Vec (x:xs) a decomposition, where we just iterate over the current x dimension, and then recurse into the next level to Vec xs a, applying the function f when we reach Vec [] a.
The exact same goes for a vec<1, 2, 3, 4, int> template in C++.
I’m terrible at templates, but I had my go at it:
First we create a structure that holds some data recursively, including an array of ourselves with one dimension removed, up until we run out of dimensions.
template <typename T, size_t ...Xs>
struct MArray;
template <typename T, size_t X, size_t ...Xs>
struct MArray<T, X, Xs...> {
std::array<MArray<T, Xs...>, X> data;
};
template <typename T>
struct MArray<T> {
T data;
};
Then we create something to traverse this object with:
template <typename T, size_t ...Xs>
struct ForEach_;
template <typename T, size_t X, size_t ...Xs>
struct ForEach_<T, X, Xs...> {
void operator () (auto f, MArray<T, X, Xs...> &array) {
for (size_t i = 0; i < X; ++i) {
ForEach_<T, Xs...> m;
m(f, array.data[i]);
}
}
};
template <typename T>
struct ForEach_<T> {
void operator () (auto f, MArray<T> &array) {
f(array.data);
}
};
Don’t ask why this is an object with an
operator (), I’m just too used to seeing them everywhere.
This generates a for loop removing one level from the stack of dimensions and calling the next level’s foreach, up until we get a value which we pass to the function.
This is terrible with tons of redirection but it works and demonstrates the idea. A lot of this is also kinda pointless since we have std::mdspan and friends that already implement this better, faster, and of course cooler.
Now we can run it:
MArray<int, 2, 2> a = {{1, 2}};
ForEach_<int, 2, 2> m;
m([](auto val){ std::println("{}", val); }, a);
m([](auto &val){ ++val; }, a);
m([](auto val){ std::println("{}", val); }, a);
I think that to reach “Rank Polymorphism”, we would also need a way to wrap a function to either behave normally on values, or map over elements in arrays, which should be more or less easily doable via reflection. Just asking “are you of higher-rank” and either calling the function directly or wrapping the call.
Footnotes🔗
1 No pun intended
2 A lot
3 And without using dynamic types anywhere.