Why can't I "template" virtual member functions?
Setting the scene...
This is something I have wanted to do a few times throughout my C++ journey, which started about a year ago. Only when I started to understand templates and virtual functions did it become clear why this is not currently possible. Although it will be nice if the standard introduced it.Suppose we have the following data structures
struct name { std::string first, middle, last; }; struct address { int door; std::string street; }; //...
and the following Io modules and worker class
class worker { public: template<typename T> void work(T& data) { std::cout << "work! : " << typeid(data).name() << std::endl; } }; class Io { protected: std::unique_ptr<worker> w; public: Io() : w(new worker){} template<typename T> void push(T& data) { std::cout << "from Io" << std::endl; w->work<T>(data); } }; class XmlIo : public Io { public: XmlIo() : Io(){} template<typename T> void push(T& data) { std::cout << "extra preprocessing" << std::endl; std::cout << "from XmlIo" << std::endl; w->work<T>(data); } };
Suppose that we pass some driver function command line arguments which dictates what type of Io we are accepting and with this comes some extra processing of the data, changes to external state etc. then we need to choose our subclassed Io module at run-time. Here is a simplified scenario.
int main(int argc, const char * argv[]) { // say, we used the factory method // or bridge "pimpl" pattern? Io* io = new XmlIo(); address anthony {234, "Crown Lane"}; // we don't get the extra preprocessing! io->push(anthony); return 0; }
The output shows that we are not getting what we want! Well, that's obvious : We have not told the compiler that the method is virtual (commonly referred to as method hiding), so let's do that. As we can see there is another problem the compiler is telling us that we can't make it virtual. Why?
How do templates work?
When the compiler encounters template code it just performs syntax checking. That is it. Nothing else. UNTILL the compiler encounters a specific instantiation of the template method in the code. Then and only then does it generate the necessary code. So essentially, templates are just fancy compile time copy and paste. The take home message is that this is done at compile-time.How to virtual methods work?
On compiling a C++ class, a binary object is created that contains all methods for the class. If a method is not marked as virtual then the implementation is hard coded where the method is called. This is called static binding.
If the method is marked as virtual a vtable (virtual table) is generated, which is essentially a map of (type -> implementation). Each instantiation of the class has a pointer to the table. When a virtual method is called on such an object the pointer is followed to the table and the appropriate version of the method is executed based on the actual type of the object at run time. This is referred to as dynamic binding.
So what is wrong?
The compiler just doesn't let us do it. So I have to roll the dice and make some educated guess from above. Well, io->push<name>("anthony") generates code for Io's push and nothing else at compile time. I assume that when it comes for the run-time system to discover which version of push to call given the actual type of the object, XmlIo in this case, there is just no implementation available as the compile-time system has not generated it.
Sources of confusion
Here we have been discussing concrete classes (not generic ones) with generic methods. If we had a generic class then the compiler always generates code for all virtual methods of a generic class. However, for non-virtual methods the compiler generates code only for those non-virtual methods that we actually call in our code (for a given type). This does not apply to us, but it is interesting to correctly solidify the difference. We are working with concrete classes with template methods and not template classes with (given a type) concrete methods.
Solution...?
Here is a solution.
#include<iostream> #include<memory> struct name { std::string first, middle, last; }; struct address { int door; std::string street; }; //... class worker { public: template<typename T> void work(T& data) { std::cout << "work! : " << typeid(data).name() << std::endl; } }; class Io { protected: std::unique_ptr<worker> w; public: Io() : w(new worker){} virtual void push(name& data) {w->work<name>(data);} virtual void push(address& data) {w->work<address>(data);} }; class XmlIo : public Io { public: XmlIo() : Io(){} void push(name& data) override { std::cout<<"extra xml work" << std::endl; w->work<name>(data); } void push(address& data) override { std::cout<<"extra xml work" << std::endl; w->work<address>(data); } }; int main(int argc, const char * argv[]) { // say, we used the factory method // or bridge "pimpl" pattern? Io* io = new XmlIo(); address anthony {234, "Crown Lane"}; // we don't get the extra preprocessing! io->push(anthony); return 0; }
It is ugly as hell. We have to change both our base class and subclass(es) if we want to add some functionality. This is a very convoluted example, but situations like these do crop up. It would be nice to see C++ solve this in some way.
Comments
Post a Comment