Extending STL Utilities with the Power of Private Inheritance
- Joshua Langley
- Nov 28, 2021
- 3 min read
Introduction
The C++ STL provides a number of useful types which can enable type erasure without having to resort to C++'s somewhat clunky implementation of the vtable. The one of interest for this exercise is std::function, which has a specialization for function signatures that allows us to erase the type of anything which can be called with the matching signature.
#include <functional>
#include <utility>
#include <iostream>
using IntegerHandler = std::function<void(int)>;
int main() {
// In this case we are just using a lambda but it works with
// raw function pointers and non-anonymous types as well
IntegerHandler handler1{
[](int v){
std::cout << std::to_string(v) << std::endl;
}
};
}
This utility is fantastic as it handles the coercion of the type and allows developers to very quickly erase the type when passing to containers which may need homogeneous types.
One of the shortcomings however is that it only accepts a single type parameter and doesn't offer the ability to support multiple signatures. In the next example we will show how we can potentially extend the STL to implement this functionality without much code.
Implementation
We can implement this by leveraging std::function and creating a wrapper which allows a parameter pack of return types and arguments:
#include <functional>
#include <iostream>
namespace std {
template <typename ...T>
struct multifunction : std::function<T>...{
template <typename F>
multifunction(F&& func) : std::function<T>{func}... {
}
using std::function<T>::operator()...;
};
}
template <typename ...T>
struct overload : private T... {
overload(T&& ...func) : T{func}... {}
using T::operator()...;
};
int main() {
std::multifunction<void(int, int), void(std::string)> function{
[](auto... vals){
[[maybe_unused]] int dummy[] = {
(std::cout << vals << ", ",0)...
};
std::cout << std::endl;
}
};
function(1, 2);
function("1");
}
Breakdown
There are two core types introduced in te above example std::multifunction and overload. The Overload type is a common utility which crops up very regularly in production code and there is a fantastic talk breaking it down. The way that std::multfunction works in much the same way but uses std::function as an interface which allows us to declare multiple function signature types. You may notice that we didn't worward the function that multifunction is passed to std::function, the reaon for this is that whilst we want to recieve a reference of any type we want it to remain as an l-value and we do not want to move it but rather copy it to each std::function declaration.
For the above example I am using overload to create an unnamed type which inherits from the lambdas and implements their callable operators but the same would work with a custom class which has more than one callable operator. I received a few questions of the use of the dummy integer array in the variadic lambda. Ths is a small technique that can be used to perform arbitrary operations on each member of a parameter pack by abusing the comma operator.
The comma operator is an overloadable operator in C++ but the default implementation of it will execute the expression on the left hand side of the comma but ignore the result of the expression and return the value of the expression of the right. This often trips up python programmers as the comma operator looks an awful lot like a tuple syntax.
[](auto... vals){
[[maybe_unused]] int dummy[] = {
(std::cout << vals << ", ",0)...
};
std::cout << std::endl;
}
The result of the above is actually an unused array of 0 which has the same length as the number of arguments passed to the lambda.
Conclusion
As you can see private inheritance is a powerful tool especially when combined with Template Meta-Programming and is an effective way of implementing a has-a relationship. Though there is a word of warning when inheriting from STL types as mos STL types do not have virtual destructors. This means that if the class you are inheriting for is non trivial it will not been cleaned up correctly and cause memory leaks and oter defined behaviours. This is more a problem when inheriting from STL Containers which have complex heap allocated memory that may not be destructed correctly.
Commentaires