2
\$\begingroup\$

Background

This is a follow-up of this CR: Compile-time plugin system. The code exposed below can also be browsed on this gist, which shows the differences between both questions (original is version 1, this one is version 3).

This is a simple factory which is able to serve the requested specific implementation of an interface on-request. The idea is to find a mechanism that doesn't require to change a common file registering all the known implementations, but rather that each files containing an implementation can register itself by the simple fact of being compiled-in.

Here, "plugins" are just cpp files, as in the original project, it was not an option to compile a dynamic library and load the plugins from them. That's why it's a "compile-time plugin", though the word "plugin" may not make much sense in this case...

This CR also refers to this SO question: Compile-time plugin / Automated Factory Registration with separate modules constraint

The mechanism

plugins.h

#include <list>
#include <string>
#include <map>
#include <memory>

namespace PluginSystem {

    /* Base class for plugins */
    class IPlugin {
    public:
        virtual void DoSomething() = 0;
    };

    /* 
     * Base class for PluginRegistrar
     * See PluginRegistrar below for explanations
     */
    class IPluginRegistrar {
    public:
        virtual std::unique_ptr<IPlugin> GetPlugin() = 0;
    };

    /* 
     * This is the factory, the common interface to "plugins".
     * Plugins registers themselves here and the factory can serve them on
     * demand.
     * It is a Singleton
     */
    class PluginFactory {
    public:
        /* Get Singleton instance */
        static PluginFactory& Instance();
        /* Register a new plugin */
        void Register(IPluginRegistrar* registrar, std::string name);
        /* Get an instance of a plugin based on its name */
        /* throws out_of_range if plugin not found */
        std::unique_ptr<IPlugin> GetPlugin(std::string name);

    private:
        /* Holds pointers to plugin registrars */
        std::map<std::string, IPluginRegistrar*> registry_;
        /* Make constructors private and forbid cloning */
        PluginFactory(): registry_() {};
        PluginFactory(PluginFactory const&) = delete;
        void operator=(PluginFactory const&) = delete;
    };

    /* 
     * Helper class that registers a plugin upon construction.
     * Actually, the registrar registers itself, and the proxied plugin is only
     * created on-demand. This mechanism can be shortened by directly 
     * registering and instance of the plugin, but the assumption here is that
     * instanciating the plugin can be heavy and not necessary.
     */
    template<class TPlugin>
    class PluginRegistrar: public IPluginRegistrar {
    public:
        PluginRegistrar(std::string classname);
        std::unique_ptr<IPlugin> GetPlugin();
    private:
        /* That is not really used there, but could be useful */
        std::string classname_;
    };

    /* template functions in header */

    template<class TPlugin>
    PluginRegistrar<TPlugin>::PluginRegistrar(std::string classname): classname_(classname) {
        PluginFactory &factory = PluginFactory::Instance();
        factory.Register(this, classname);
    }

    template<class TPlugin>
    std::unique_ptr<IPlugin>
    PluginRegistrar<TPlugin>::GetPlugin() {
        std::unique_ptr<IPlugin> plugin(new TPlugin());
        return plugin;
    }
}

/*
 * Here is the trick: upon creation of the global variable, the class created
 * out of the template will get instanciated once, and will register itself.
 * The template contains the information to create a plugin instance.
 * An unnamed namespace is used to enclose this later unused variable in the
 * compilation unit.
 */
#define REGISTER_PLUGIN(CLASSNAME) \
    namespace { \
        static PluginSystem::PluginRegistrar<CLASSNAME> \
        _registrar( #CLASSNAME ); \
    };

plugins.cpp

#include "plugins.h"

namespace PluginSystem {

PluginFactory&
PluginFactory::Instance() {
    static PluginFactory instance;
    return instance;
}

void
PluginFactory::Register(IPluginRegistrar* registrar, std::string name) {
    registry_[name] = registrar;
}

std::unique_ptr<IPlugin>
PluginFactory::GetPlugin(std::string name) {
    /* throws out_of_range if plugin unknown */
    IPluginRegistrar* registrar = registry_.at(name);
    return registrar->GetPlugin();
}

}

How to use it

plugin1.cpp

/* Could be also split in .h/.cpp, here kept in one file for simplicity */
#include <iostream>
#include "plugins.h"

class Plugin1: public PluginSystem::IPlugin {
    void DoSomething() {
        std::cout << "Plugin1" << std::endl;
    }
};

REGISTER_PLUGIN(Plugin1)

main.cpp

#include <iostream>
#include "plugins.h"

int main()
{
    auto &factory = PluginSystem::PluginFactory::Instance();

    auto plugin = factory.GetPlugin("Plugin1");

    plugin->DoSomething();

    return 0;
}

Changes

  1. I dropped support for VS 2008, because I feel for a real complete review and a good SO answer, one has to use the latest technologies. So I tried to stick to C++11 and tested with GCC. Anyway, our project is now moving to VS 2015.
  2. Concerning the interface problem raised by Loki Astari, I made the choice, for this example, that the factory could be asked to return several instances of the same plugin. So I now return a unique_ptr to signify ownership transfer, but not references because neither the registrar nor the factory is supposed to keep track of the plugin instance. This was especially important, as in the first version, the plugin instance would have actually leaked.
  3. About how the registrar is registered, I use raw pointers, the factory doesn't steal the ownership. The registrar being a global variable, it is always available. I felt using a shared_ptr and weak_ptr wasn't necessary and heavy for no reason. But maybe it's the wrong choice?
  4. I kept the macro because I feel it gives sense to a construct that is otherwise really strange (why would you instantiate a registrar to register a plugin? The construct is odd, but the macro gives it a meaning). I had to change its implementation though, because the previous one would fail if used with a fully qualified name (i.e. namespace::plugin).

Questions

As for last time, please be pedantic as you wish, I wanna learn. It's been 10 years I did not do proper C++ (I usually code embedded C++ which is nothing more than C + classes).

The main point to my eyes is the handling of ownership, and a second one is the mix of references and (smart-)pointers. For example in main, the differences on how to use factory and plugin, which feels like an unbalanced convention. Also, is there an explicit way to declare that an object retains ownership on a value, rather than a raw pointer that can actually express this, but as well indicate that maybe the author did not think about it at all (like I did in the first version).

I hope I did not harass you with this follow-up post!

\$\endgroup\$
  • \$\begingroup\$ The only big thing that I see that is missing here is that both of your interface classes lack a virtual destructor. In general, they should always be present when polymorphic behaviour is in play: stackoverflow.com/questions/461203/… \$\endgroup\$ – glampert Feb 16 '16 at 17:55
  • \$\begingroup\$ Also, we like follow-up questions here on CR ;) \$\endgroup\$ – glampert Feb 16 '16 at 17:55
  • \$\begingroup\$ @glampert: Thanks for you reply. I guess this is a valid answer, so if you post it I will be able to upvote. \$\endgroup\$ – Cilyan Feb 17 '16 at 21:46
  • \$\begingroup\$ I'm afraid that's not enough content for a proper answer, nevertheless, the advice is given, that's what counts! But if no one else comes up with an answer, I might think of some other minutiae to add up for an answer of mine ;) \$\endgroup\$ – glampert Feb 17 '16 at 22:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.