At current, c++ has so many different ways to do polymorphism (in my understanding, polymorphism is a way to write same code for different types). Too many ways means too complexed,
so we hope we can do all kind of polymorphism in an unified way.
To save time, I will not discussion all the different ways of polymorphism, but focus on two most representative ways of polymorphism: virtual abstract class and template function.
Normally, if we can do polymorphism by virtual abstract class, we can do same thing by template function.
For example, we do polymorphism by virtual abstract class like below:
class rectangle_like {
public:
virtual int getWidth() const = 0;
virtual int getHigh() const = 0;
};
struct rectangle : public rectangle_like {
int getWidth() const {
return width;
}
int getHigh() const {
return high;
}
int width;
int high;
};
struct square : public rectangle_like {
int getWidth() const {
return side;
}
int getHigh() const {
return side;
}
int side;
};
int area(const rectangle_like& t) {
return t.getWidth() * t.getHigh();
}
int main() {
rectangle r{};
area(r);
square s{};
area(s);
}
Then similar code can be written by template function:
struct rectangle {
int getWidth() const {
return width;
}
int getHigh() const {
return high;
}
int width;
int high;
};
struct square {
int getWidth() const {
return side;
}
int getHigh() const {
return side;
}
int side;
};
template <typename T>
int area(const T& t) {
return t.getWidth() * t.getHigh();
}
int main() {
rectangle r{};
area(r);
square s{};
area(s);
}
As the example shown, we do polymorphism (write the area function) by both virtual abstract class and template function. In fact, ignore type erasure, if polymorphism can be done by
virtual abstract class, the polymorphism can be done by template function, too. However, there are some polymorphism that can be done by template function, but cannot be done by virtual abstract class. For example, we can write selfCompare by template function:
template <typename T>
bool selfCompare(T t) {
return t == t;
}
int main() {
selfCompare(int{});
selfCompare(double{});
}
But we cannot do same thing by abstract class directly. If we do that force, the code will be like:
class comparable {
public:
virtual bool compare(const comparable&) const = 0;
};
class MyInt : public comparable {
bool compare(const comparable& c) const {
auto mi = dynamic_cast<const MyInt&>(c);
assert(&mi != nullptr);
return val < mi.val;
}
int val;
};
bool selfCompare(const comparable& c) {
return c.compare(c);
}
We have to do dynamic_cast and assert even though we always compare two same objects with same type, which makes the code time consuming and hard to maintain. I have studied many c++
dynamic dispatch libs, all of them have same problem.
But we have an interesting tricky to solve the problem, looking at the template function:
template <typename T>
bool selfCompare(T t) {
return t == t;
}
If we do some tiny changes on it: 1. Replace T to T&; 2. Pass compare function explicitly. Then the function will be like:
template <typename T>
bool selfCompare(const T& t, bool(*comparable)(const T&, const T&)) {
return comparable(t, t);
}
After that, the template function works for incomplete type:
class incomplete;
auto selfCompareErase = selfCompare<incomplete>;
What’s more, incomplete type is always pointer/reference, pointer/reference to different type is always bit wise cast safety. So, every type can be converted to incomplete type, the
conversion code will be like this:
template <typename T>
struct Comparable {
const T& t;
bool(*compare)(const T&, const T&);
};
template <typename T>
ErasedComparable erase_comparable(const Comparable<T>& comparable) {
return std::bit_cast<ErasedComparable, Comparable<T>>(comparable);
}
So, we write a template function selfCompare, it works for static polymorphism, then we have a function selfCompareErase works for dynamic polymorphism automatically, that’s why I call
it as unified polymorphism.
Here is an more complexed example, in the example, we can even sort an array:
https://godbolt.org/z/TPdK683hY.
The unified polymorphism has these advantages:
Compare to other languages, all the function programming languages support such usage, what’s more, Swift can support such usage, too. (https://godbolt.org/z/4jWveKePx)(Rust
cannot do that, for rust has object safety limitation.)
At current, it’s just an idea, not sure whether it will be a lib or language core.
Please let me know if there are any good advice for that.