Many years ago I had a non-technical manager who was amazing. If you’ve been in the field long enough you’ll know that this is a rare and wonderful thing. One of the things that made this guy special was how he handled the yearly review process. Maybe I should tell you how every other company’s review process works so you can compare and contrast. Here are the steps for every other company:
- Fill out a humiliating form boasting about everything you did during the year because it’s apparent that your manager barely remembers who you are. Make sure you add a section about your failures so everyone will know you’re humble.
- Meet with your manager so he can tell you how amazing you are for a while.
- Then learn that despite how amazing you are and how awesome the company is, learn that there’s no money for raises because of convoluted financial magic.
- Receive disappointing raise with vague talk about how it might possibly be larger next year.
So compare that with my amazing manager who absolutely knew who you were and had clearly done a lot of work to create a review process that he thought would be useful. No forms to fill out. He’d ask you to come up with a handful of technical goals you wanted to achieve, and a handful of non-technical goals. That’s right, not only did he want to help you become a better programmer, he’d also play coach for anything else you wanted to do. Some people came to him with lists of things like “learn to cook” or “get motorcycle license” and he’d absolutely help you set milestones to accomplish those goals too.
So one year when it was my turn for the yearly review, I told him I wanted to learn how to use Design Patterns. Like most every programmer, I’d read the Design Patterns book, but I never could figure out why it was useful. I basically memorize them all when it’s time to interview for a new job, and promptly forget about them. So my manager gave me the task of learning a new design pattern that wasn’t in the book. This sounded interesting enough and after a little research, I came across what I call the Switch on Type Construction pattern, and this (finally) brings me to the point of this post.
Warning: I was recently horrified to learn that this solution does not always work, depending on compilers, optimizers, environments, etc. But it’s still awesome so I’m keeping the post. But be careful with it.
Often in C++ you end up in a situation where you want your code to do something different based on the type of the class. This is called “Switch on Type” and it is a “bad thing.” This phenomenon is so well known it’s a classic case of why we have classes in the first place. Here’s some code to demonstrate, let’s say you have something like this to handle images:
void render( Image* obj ) { if (get_extension(obj->file_name()) == "jpg") { obj->render_jpg(); } else if (get_extension(obj->file_name()) == "png") { obj->render_png(); } }
This is bad because as the list of formats grows, you’re constantly back in here cutting and pasting to the list, and probably introducing bugs, etc. This situation was solved with virtual functions: a base image type, and then a subclass of Jpg and Png image types. So the previous code just turns into something obvious like:
obj->render_image();
where the render_image
function is virtual in the base type, and then overridden to do the correct things in the subclass.
This is all well and good, but what do we do with object creation? You can’t use virtual functions in a class that hasn’t been constructed, and there’s no such thing as a virtual constructor. Our task for this post will be to create an appropriate image object depending on the file type (and we’ll assume that the type is correct and ignore errors).
So let’s take our starting point as this:
class Image { /* blah blah constructors, functions etc */ }; class Png_image : public Image { /* blah blah */ }; class Jpg_image : public Image { /* blah */ }; std::shared_ptr<Image> create_image( const std::string& file_name ) { if (get_extension(file_name) == "jpg") { return std::shared_ptr<Image>( new Jpg_image( file_name ) ); } if (get_extension(file_name ) == "png") { return std::shared_ptr<Image>( new Png_image( file_name ) ); } throw std::runtime_error( "Unknown image type" ); }
Does that seem like some code you may have written in the past? I know it’s definitely something I have written. Any why not? It’s short, concise, solves the problem, etc etc. Who cares that every time we need to add a new image type we have to update this function? Well what if I told you there’s a pattern that lets you add new image types without recompiling existing code? “Impossible” you’d say, and you’d be wrong.
The first thing we’ll need to create for our awesome solution is an object creation map. This is a map that uses our image type (either “png” or “jpg”) as a key and a creation function as a value. With this we can simply use the image type to find the correct creation function. Of course this map will need to be a singleton as we only want one of them in the whole program. Let’s expand our Image class to include:
- Typedefs for complex types
- The creation map
- Accessors to this map
class Image { public: static void register( const std::string& ext, Creation_func& func) { function_map()[ext] = the_function; } static std::shared_ptr<Image> create( const std::string& file_name ) { return function_map()[get_extension(file_name)]( file_name ); } private: typedef std::function<std::shared_ptr<Image>(const std::string&)> Creation_func; typedef std::map<std::string, Creation_func> Function_map; static Function_map& function_map() { static Function_map singleton_map; return singleton_map; } };
First consider the Creation_func typedef. It defines a function signature for a function that creates shared_ptrs of Image. Such a function might look like std::shared_ptr<Image> create( const std::string& file_name )
. This Creation_func is combined with a file extension (ie “png” or “jpg”) to make a Function_map. Then there’s a function_map() accessor, and a function to add entries to the map, and a function to create an actual image. Let’s see how our code could now use this to get rid of the “switch on type” issue:
// functions defined elsewhere std::shared_ptr<Image> create_jpg( const std::string& file_name ); std::shared_ptr<Image> create_png( const std::string& file_mame ); // register them in the map Image::register( "jpg", create_jpg ); Image::register( "png", create_png ); // and use them in your code std::shared_ptr<Image> jpg_image = Image::create( "file_name.jpg" );
So that’s a pretty cool start. We no longer have the if (extension_check) { return Image; }
mess that we had before. But it’s still not terribly easy to use. Now if we wanted to add support for a third image type we’d still have to edit our code and add a call to register
. Still, we could stop right here and have a much better solution… or we could add templates to make the whole thing better.
Enter Templates
What we want to do to make this solution better is to create a system wherein new image types can be added to the code without recompiling anything. That’s sort of the holy grail of system extension. That means no testing of any existing code because the existing code won’t change. Heck you could take your existing object files, compile in the new image classes, link the whole thing together and magically have support for new types. Sounds like a trick to strive for. So to help us out we want to add a template class that acts sort of like a factory. Here’s how it looks:
template <class T> struct Registration_object { Registration_object( const std::string& ext ) { Image::Creation_func f = Registration_object<T>::create; Image::register( ext, f ); } static std::shared_ptr<Image> create( const std::string& image_name ) { return std::shared_ptr<Image>( new T( image_name ) ); } };
So what’s going on with this one? Well it has a static create
function that simply creates a new Image of the templated type T. It also has a constructor that will add a pointer to that create
function into the singleton function map.
So how do we use this new registration object? All we have to do is create exactly one of these for each image type, and it will automatically register that new type in the map. So let’s change our usage code to use these instead:
Registration_object<Jpg_image> jpg_registration( "jpg" ); Registration_object<Png_image> png_registration( "png" ); std::shared_ptr<Image> jpg_image = Image::create( "file_name.jpg" );
That’s starting to look pretty awesome. Now we just have to create one global variable that we never directly use, and our image type is magically added to the map. So how can we use this trick to add an entirely new image type without recompiling existing code? Hold on to your hats, cause this is the amazing bit. I’m not going to touch the code above (ie I will not recompile it) but I can add Gif support by simply adding the following in a separate .cpp file:
class Gif_image : public Image { public: Gif_image( const std::string& file_name ) { /* Gif image stuff*/ } }; Registration_object<Gif_image> gif_registration( "gif" );
Did you see it? Now when this new .cpp file is compiled and linked with the existing object files, your program will suddenly be able to deal with gif images. The reason this works is during static object creation time (before main starts) an instance of this new Registration_object<Gif_image> will be constructed, and during that construction it will register its create function with the static function map. If that doesn’t impress you then stop reading this blog and fuck you.
Oh, and what about that manager I had? He helped me reach a number of goals, both technical and non. Eventually the company merged with another one and he was replaced with the HR lady from the other company. The first thing she did was bring in those damned yearly evaluation forms like every other company has. I was more then happy to quit that job.