I'm exploring the new deducing feature introduced in C++23, and I'm trying to understand how to combine it effectively with the Curiously Recurring Template Pattern (CRTP) to implement polymorphic behavior without relying on traditional virtual functions.
My goal is to manage a collection of derived objects (e.g., shapes, processors, etc.) in a type-erased or semi-polymorphic way, and still get the benefits of static polymorphism and possibly reduce indirection and dynamic allocation.
#include <vector>
#include <iostream>
template <typename Derived>
struct Base {
void process(this Derived& self) {
self.impl(); // supposed to call derived's impl()
}
};
struct DerivedA : Base<DerivedA> {
void impl() {
std::cout << "DerivedA::impl\n";
}
};
struct DerivedB : Base<DerivedB> {
void impl() {
std::cout << "DerivedB::impl\n";
}
};
int main() {
std::vector</* ??? */> collection;
collection.push_back(DerivedA{});
collection.push_back(DerivedB{});
for (auto& obj : collection) {
obj.process(); // call the correct impl() for each type
}
}
I’m aware that std::variant
is a way to handle this, but I'm looking for alternative high-performance solutions. Ideally, I’d like to avoid the overhead of dynamic allocations and virtual functions, and use modern C++23 features like deducing this with CRTP to get static polymorphism benefits.
you cannot use deducing this or CRTP for this, deducing this
only made writing CRTP code easier, it didn't change the fact that you cannot have a vector of CRTP types, and you cannot have a vector of heterogenous types, you can have a vector of variants, or a vector of pointers to heterogenous types base std::vector<std::unique_ptr<Base>>
(Base is not templated).
I’d like to avoid the overhead of dynamic allocations and virtual functions
your two options are
std::vector<std::variant<...>>
std::tuple<<std::vector<...>, ...>
basically a vector for each type, when order is not important, or you can store the order externally if it is needed, boost::base_collection
does something similar, and it has no order.as an optimization over std::vector<std::variant<...>>
you can see How to Design a Slimmer Vector of Variants in C++ - Christopher Fretz - CppCon 2024, his design removes most of the space overhead of heterogenous types while keeping them contigious by storing the type and offset of each element externally. (you can compact both into 8 bytes, or actually less than 8 bytes)
Note: the overhead of virtual functions and visiting a variant are comparable, with std::visit
being usually slower than a virtual function because std::visit
has to handle valueless_by_exception
. what you should be worried about is bad cache locality and branch misprediction, see The Hidden Performance Price of C++ Virtual Functions - Ivica Bogosavljevic - CppCon 2022 , hence if the order is not important then just use boost::base_collection
or roll a tuple of vectors.
For completeness sake, you can apply type-erasure and small buffer optimization like how std::function
works, there are implementers for generic types like dyno, but if you carelessly apply this "optimization" without measuring how much benefit you will actually get, you may end up not using the SBO, and this "optimization" would make your code slower and use more memory with no benefits.